PageSourceSearch

https://www.phoenix-ambulanz.de/js/_essentials/ruffnav.js

js phoenix-ambulanz.de collected 2026-10-06 15:25:37 UTC 42,547 bytes, 854 lines download raw bytes

1/**
2 * off-canvas nav
3 * @name ruffnav
4 * @author Dominik Kressler
5 * @version 2.7.1
6 * @thanks Jasmin Reiff
7 * @edit by Malte Zoudlik
8 * @edit by Dario Rieke
9 * @edit by Sami El-Younsi
10 * @edit by Bartosz Adamski
11 * @edit by Daniel Regiment
12 * @lastedit 2025-09-17
13 * @copyright (c) 2018
14 */
15class Ruffnav {
16    constructor(options) {
17        this.options = {
18            backText: false,
19            container: '#navigation',
20            closeOnScroll: true,
21            desktopHover: false,
22            mobileBreak: window.cssData.break,
23            navAppend: false,
24            navList: '.navi-main',
25            subClass: '.sub',
26            subParent: '.has-sub',
27            toggler: '#navi-toggled',
28            toggleLabel: 'label[for="navi-toggled"] [role="button"]',
29            navSplit: false,
30            forceMobileNav: false
31        };
32
33        this.options = { ...this.options, ...options };
34
35        this.activePath = '';
36        this.alreadyOpened = false;
37        this.container = document.querySelector(this.options.container);
38        this.depth = 0;
39        this.device = breakpoint(this.options.mobileBreak) ? 'desktop' : 'mobile';
40        this.html = document.documentElement;
41        this.isOpen = false;
42        this.lang = this.html.lang.split(/[-_]/)[0].toLowerCase();
43        this.navigationStatus = document.querySelector('[data-navigation-status]');
44        // both selectors are needed to maintain backwards compatibility for websites creates with
45        // initial files > 1.8.0
46        this.navList = this.container.querySelector(`${this.options.navList} > ul`) ?? this.container.querySelector(this.options.navList);
47        this.navSize = 0;
48        this.path = window.location.pathname.split('/').filter(el => el !== '');
49        this.sub = this.container.querySelectorAll(this.options.subClass);
50        this.subMenuPositioner = null;
51        this.subParent = this.container.querySelectorAll(this.options.subParent);
52        this.subToggler = null;
53        this.toggler = document.querySelector(this.options.toggler);
54        this.windowSize = window.innerWidth;
55        
56        // Initialize navigation height updater immediately
57        this.navigationHeightUpdater = new NavigationHeightUpdater(this.container);
58
59        // Initialize SubmenuPositioner immediately if submenus exist
60        if (this.sub.length > 0) {
61            // Specify submenus to exclude from repositioning if any (empty array for now)
62            const excludedNames = [];
63            this.subMenuPositioner = new SubmenuPositioner(30, excludedNames, this.sub, this);
64        }
65
66        // set back button text by language, or by overwrite
67        if (this.options.backText === false) this.options.backText = (this.lang === 'de') ? 'Zurück' : 'back';
68
69        this.init();
70    }
71
72    init() {
73        if (!this.navList) {
74            console.warn(`No element found with class "${this.options.navList}". Ruffnav couldn't be initialized. Please add the class to your main navigation UL-Element`);
75            return;
76        }
77
78        this.backButton = this.create('li', {
79            'class': 'nav-back',
80            'tabindex': '0',
81            'aria-label': this.options.backText,
82            'role': 'button'
83        }, `<span>${this.options.backText}</span>`);
84
85        document.querySelector(this.options.toggleLabel).addEventListener('keydown', function(e) {
86            if (e.key === "Enter" || e.key === ' ') {
87                e.preventDefault();
88                this.click();
89            }
90        })
91
92        // add class for desktop hover
93        this.container.classList.add('first-level');
94        if (this.options.desktopHover) this.container.classList.add('hover-navigation');
95
96        // aria-attribute for nav element
97        const navigationName = (this.lang === 'de') ? `Hauptnavigation` : `main navigation`;
98        this.container.firstElementChild.setAttribute('aria-label', navigationName);
99
100        // remove expanded attribute for desktop navigation
101        let deviceType = breakpoint(this.options.mobileBreak) ? 'desktop' : 'mobile';
102        if (deviceType === 'desktop') this.container.removeAttribute('aria-expanded');
103
104        // append custom Content to Navigation
105        if (this.options.navAppend) {
106            const itemToAppend = this.create('li', { 'class': 'nav-item appended-item' }, this.options.navAppend);
107            this.navList.appendChild(itemToAppend);
108        }
109
110        this.navSize = this.navList.children.length;
111
112        // transform Navigation markup
113        this.subParent.forEach(parentItem => {
114            const childNode = parentItem.firstElementChild;
115            const categoryLabel = (childNode.dataset && childNode.dataset.categoryLabel) ? childNode.dataset.categoryLabel : childNode.innerHTML.replace(/(<([^>]+)>)/gi, "");
116
117            // parent-name attribute for UL's
118            let subParentName = childNode.textContent.replace(/(\r\n|\n|\r)/gm, '');
119            childNode.nextElementSibling.setAttribute('data-parent-name', subParentName.trim());
120
121            // only clone when Element is a link
122            if (childNode.nodeName !== 'SPAN') {
123                const activeLinkClass = (childNode.classList.contains('active') && window.location.origin + window.location.pathname === childNode.href) ? 'active' : '';
124                const nodeLinkClass = childNode.className.trim().replace(/\bactive\b/g, '').trim();
125
126                // copy link inside UL's
127                childNode.nextElementSibling.insertBefore(this.create('li', {
128                    'class': 'nav-item ' + childNode.className.trim()
129                }, `<a href="${childNode.href}" title="${childNode.title}" class="${activeLinkClass} ${nodeLinkClass}">${categoryLabel}</a>`), parentItem.children[1].children[0]);
130
131                if (this.path.length) this.activeClassFallback(childNode);
132            }
133
134            // replace link's with span's
135            const label = (this.lang === 'de') ? `${categoryLabel} Unterverzeichnis öffnen` : `open ${categoryLabel} sub-navigation`;
136
137            const hasClass = !!childNode.className;
138            const rawParentName = parentItem.parentElement?.dataset?.parentName || '';
139            const parentNameAttribute = rawParentName.replaceAll(" ","-").toLowerCase().trim();
140            const childName = childNode.innerHTML.replaceAll(" ","-").toLowerCase().trim();
141            const className = hasClass ? childNode.className.trim() : `${parentNameAttribute}-${childName}`;
142
143            if (!hasClass && parentItem.classList.contains('has-sub')) {
144                parentItem.classList.add(className);
145            }
146
147            const attrs = {
148                class: `sub-toggler ${className}`,
149                tabindex: 0,
150                'aria-haspopup': 'true',
151                'aria-expanded': 'false',
152                'aria-label': label,
153                role: 'button',
154                ...(hasClass && { 'data-href': childNode.href })
155            };
156
157            parentItem.replaceChild(this.create('span', attrs, childNode.innerHTML), childNode);
158        });
159
160        // navigation split
161        if (this.options.navSplit) {
162            // add a class to the element which will split the navigation 
163            this.navList.classList.add("nav-split");
164            const middle = Math.round(this.navSize / 2);
165            if(middle) {
166                this.navList.children.item(middle - 1)?.classList.add("nav-split-li");
167            } 
168        }
169
170        // add aria-attributes and back buttons for all submenus
171        this.sub.forEach(subItem => {
172            subItem.setAttribute('aria-expanded', 'false');
173            const subMenuLabel = (this.lang === 'de') ? `${subItem.dataset.parentName} Unterverzeichnis` : `${subItem.dataset.parentName} sub-navigation`;
174            subItem.setAttribute('aria-label', subMenuLabel);
175
176            // add tabindex for mobile navigation to make submenu focusable
177            if (deviceType === 'desktop') {
178                subItem.removeAttribute('tabindex');
179            } else if (deviceType === 'mobile') {
180                subItem.setAttribute('tabindex', '0');
181            }
182
183            // add back buttons to UL's and add aria-label 
184            const thisBackButton = this.backButton.cloneNode(true);
185            const backButtonLabel = (this.lang === 'de') ? `${subItem.dataset.parentName}
185 Unterverzeichnis schließen` : `close ${subItem.dataset.parentName} sub-navigation`;
186            thisBackButton.setAttribute('aria-label', backButtonLabel);
187            subItem.insertBefore(thisBackButton, subItem.childNodes[0]);
188        });
189
190        // add aria attributes to each link inside navigation
191        const allNavigationLinks = this.navList.querySelectorAll('a');
192        allNavigationLinks.forEach(naviLink => {
193            // update / set aria-attributes for current location or extern links
194            if (naviLink.classList.contains('active')) {
195                let naviLinkName = naviLink.textContent.replace(/(\r\n|\n|\r)/gm, '');
196                const activePageLabel = (this.lang === 'de') ? `${naviLinkName} (aktuelle Seite)` : `${naviLinkName} (current location)`;
197                naviLink.setAttribute('aria-label', activePageLabel);
198                naviLink.setAttribute('aria-current', 'page');
199            } else if (naviLink.getAttribute('target') === '_blank') {
200                let naviLinkName = naviLink.textContent.replace(/(\r\n|\n|\r)/gm, '');
201                const externPageLabel = (this.lang === 'de') ? `${naviLinkName} (öffnet eine neue Seite)` : `${naviLinkName} (opens a new window)`;
202                naviLink.setAttribute('aria-label', externPageLabel);
203            }
204        });
205
206        // close on mobile toggle
207        this.toggler.addEventListener('click', this._closeAll);
208        // add Listeners for different events
209        this.addListener();
210        // add global resize listener
211        this.listenResize();
212    }
213
214    _open = (e) => {
215        // open event
216        let target;
217        if (e.type === 'mouseenter' && (this.device === 'mobile' || this.options.forceMobileNav)) return;
218        if (e.type === 'mouseenter') {
219            target = e.target;
220            target.childNodes[1].classList.add('current');
221            target.childNodes[1].setAttribute('aria-expanded', 'true');
222            if (this.container.querySelector('.open') != null) this.container.querySelector('.open').classList.remove('open');
223            target.childNodes[1].classList.add('open');
224            if (this.options.desktopHover) {
225                target.querySelector(':scope > .sub').setAttribute('aria-expanded', 'true');
226                target.querySelector(':scope > .sub').classList.add('current');
227            }
228        } else {
229            target = e.currentTarget;
230            const submenuToOpen = target.nextElementSibling;
231            if (!submenuToOpen || submenuToOpen.classList.contains('open')) return;
232            // close siblings
233            const sameLevel = target.parentElement.parentElement.querySelector('.current');
234            if (sameLevel && sameLevel !== submenuToOpen) this.closeRecusive(sameLevel, false);
235            // ensure only one .open
236            this.container.querySelectorAll('.open').forEach(openEl => {
237                if (openEl !== submenuToOpen) openEl.classList.remove('open');
238            });
239            // mark submenu open/current
240            submenuToOpen.classList.add('open', 'current');
241            submenuToOpen.setAttribute('aria-expanded', 'true');
242            target.setAttribute('aria-expanded', 'true');
243            // position if needed
244            if (this.subMenuPositioner && !(this.device === 'mobile' || this.options.forceMobileNav) && !this.subMenuPositioner.shouldExclude(submenuToOpen)) {
245                this.subMenuPositioner.resetPositioning(submenuToOpen);
246                this.subMenuPositioner.adjustPosition(submenuToOpen);
247            }
248            // inform screenreader
249            const currentSubName = submenuToOpen.getAttribute('aria-label');
250            this.updateNavigationStatus(`${currentSubName} ${this.lang === 'de' ? 'wurde geöffnet' : 'got opened'}`);
251            // focus submenu and observe
252            setTimeout(() => submenuToOpen.focus(), 250);
253            document.addEventListener('keydown', this.initWatchFocus);
254        }
255        if (!this.isOpen && this.device !== 'mobile') {
256            if (this.options.closeOnScroll) window.addEventListener('scroll', this._closeAll);
257            document.addEventListener('click', this._closeAll);
258            this.isOpen = true;
259        }
260        this.container.classList.remove('first-level');
261        this.scrollTop();
262    }
263
264    _close = (e) => {
265        // close event
266        let currentListTree;
267        if (e.type === 'mouseleave' && (this.device === 'mobile' || this.options.forceMobileNav)) return;
268        if (e.type === 'mouseleave') currentListTree = e.currentTarget;
269        else currentListTree = e.currentTarget.parentElement;
270
271        this.closeRecusive(currentListTree, false);
272        if (this.container.querySelectorAll('ul.current').length === 0) this.isOpen = false;
273        if (this.container.querySelectorAll('ul.current').length === 0) this.container.classList.add('first-level');
274
275        // inform screenreader via aria-live that submenu got closed
276        const currentSubName = currentListTree.getAttribute('aria-label');
277        const subStatusClosed = (this.lang === 'de') ? `${currentSubName} wurde geschlossen` : `${currentSubName} got closed`;
278        this.updateNavigationStatus(subStatusClosed);
279
280        // set focus 
281        const thisSubToggler = currentListTree.parentElement.firstElementChild;
282        thisSubToggler.focus();
283
284        // update aria-attribute
285        const target = e.currentTarget;
286        if (this.options.desktopHover && e.type === 'mouseleave') target.querySelector(':scope > .sub').setAttribute('aria-expanded', 'false');
287
288        this.scrollTop();
289    }
290
291    _closeAll = (e) => {
292        // check for toggle input and update attributes
293        (this.toggler.checked) ? this.html.classList.add('hidden-scroll') : this.html.classList.remove('hidden-scroll');
294        (this.toggler.checked) ? this.container.setAttribute('aria-expanded', 'true') : this.container.setAttribute('aria-expanded', 'false');
295        (this.toggler.checked) ? '' : this.alreadyOpened = false;
296        const toggleLabel = document.querySelector(this.options.toggleLabel);
297        const menuOpenTogglerLabel = (this.lang === 'de') ? `Hauptnavigation öffnen` : `open main navigation`;
298        const menuCloseTogglerLabel = (this.lang === 'de') ? `Hauptnavigation schließen` : `close main navigation`;
299        (this.toggler.checked) ? toggleLabel.setAttribute('aria-label', menuCloseTogglerLabel) : toggleLabel.setAttribute('aria-label', menuOpenTogglerLabel);
300        (this.toggler.checked) ? toggleLabel.setAttribute('aria-expanded', 'true') : toggleLabel.setAttribute('aria-expanded', 'false');
301
302        // inform screenreader via aria-live if navigation got closed or opened on mobile 
303        if (this.device === 'mobile' || this.options.forceMobileNav) {
304            const navigationStatusOpened = (this.lang === 'de') ? `Hauptnavigation wurde geöffnet` : `Main navigation got opened`;
305            const navigationStatusClosed = (this.lang === 'de') ? `Hauptnavigation wurde geschlossen` : `Main navigation got closed`;
306            (this.toggler.checked) ? this.updateNavigationStatus(navigationStatusOpened) : this.updateNavigationStatus(navigationStatusClosed);
307        }
308
309        // close all
310        if (e.type === 'scroll') {
311            this.closeRecusive(null, true);
312        } else if (e.type === 'click') {
313            // check if click was not inside navigation
314            if (e.target === this.navList || this.navList.contains(e.target)) return;
315            else this.closeRecusive(null, true);
316        } else if (e.type === 'keydown') {
317            // check if "enter" was pressed
318            if (e.keyCode === 27) this.closeRecusive(null, true);
319        } else {
320            this.closeRecusive(null, true);
321        }
322
323        this.removeListeners();
324        this.isOpen = false;
325    }
326
327    closeRecusive = (firstList, closeAll) => {
328        // close all open items
329        if (closeAll) {
330            this.container.querySelectorAll('.current').forEach(openSubs => {
331                openSubs.classList.remove('current');
332                openSubs.setAttribute('aria-expanded', 'false');
333                openSubs.parentNode.firstElementChild.setAttribute('aria-expanded', 'false');
334                // if the submenu was opened by keyboard (has no-transition class),
335                // remove the class AFTER the transition duration to ensure no transition on close.
336                if (openSubs.classList.contains('no-transition')) {
337                    setTimeout(() => {
338                        openSubs.classList.remove('no-transition');
339                    }, 300); // match the CSS transition duration
340                }
341            });
342
343            if (this.container.querySelector('.open') != null) this.container.querySelector('.open').classList.remove('open');
344
345            this.container.classList.add('first-level');
346        } else {
347            if (this.container.querySelector('.open') != null) this.container.querySelector('.open').classList.remove('open');
348
349            firstList.querySelectorAll('.current').forEach(openSubs => {
350                openSubs.classList.remove('current');
351                openSubs.setAttribute('aria-expanded', 'false');
352                openSubs.parentNode.firstElementChild.setAttribute('aria-expanded', 'false');
353
354                // stop observing focus inside ul
355                if (this.device === 'mobile' || this.options.forceMobileNav)  document.removeEventListener('keydown', this.initWatchFocus);
356            });
357
358            // update classes and attributes
359            firstList.parentNode.parentNode.classList.add('open');
360            firstList.classList.remove('current');
361            firstList.setAttribute('aria-expanded', 'false');
362            firstList.parentNode.firstElementChild.setAttribute('aria-expanded', 'false');
363
364            // if the submenu was opened by keyboard (has no-transition class),
365            // remove the class AFTER the transition duration to ensure no transition on close.
366            if (firstList.classList.contains('no-transition')) {
367                setTimeout(() => {
368                    firstList.classList.remove('no-transition');
369                }, 300); // match the CSS transition duration (0.3s = 300ms)
370            }
371        }
372
373        // if navigation is opened initially or reopened, set focus and observe focus inside submenu
374        if (this.toggler.checked && this.container.querySelectorAll('ul.current').length === 0 && (this.device === 'mobile' || this.options.forceMobileNav) && !this.alreadyOpened) {
375            if (this.navList.classList.contains('navi')) {
376                this.navList.classList.add('open');
377            }
378            // observe focus inside submenu
379            setTimeout(() => { document.addEventListener('keydown', this.initWatchFocus); }, 300);
380
381            this.alreadyOpened = true;
382        }
383    }
384
385    removeListeners = () => {
386        // remove eventListener for global close
387        window.removeEventListener('scroll', this._closeAll);
388        document.removeEventListener('keydown', this._closeAll);
389        document.removeEventListener('click', this._closeAll);
390        document.removeEventListener('keydown', this.initWatchFocus);
391    }
392
393    addListener = () => {
394        // add eventListener to sub-togglers and back-buttons
395        this.subToggler = this.container.querySelectorAll('.sub-toggler');
396        this.backButtons = this.container.querySelectorAll('.nav-back');
397        const ruffnavInstance = this; // Capture the Ruffnav instance
398
399        if (this.options.desktopHover) {
400            // track input modality (mouse vs keyboard) to avoid .nav-back flicker on mouse
401            this.keyboardMode = false;
402
403            this._onPointerDownForModality = function(e) {
404                // only active for desktop hover mode
405                if (!(ruffnavInstance.device === 'desktop' && ruffnavInstance.options.desktopHover)) return;
406                // if pointer is mouse, switch to mouse-mode and hide all nav-back buttons
407                // pointerType may be undefined in some browsers; fall back to checking pointer event type
408                if (e.pointerType === 'mouse' || e.type === 'mousedown' || e.type === 'mousemove') {                
409                    ruffnavInstance.keyboardMode = false;
410                    ruffnavInstance.backButtons.forEach(btn => {
411                        try { btn.style.display = 'none'; } catch (err) { /* ignore */ }
412                    });
413                }
414            };
415
416            this._onKeyDownForModality = function(e) {
417                if (!(ruffnavInstance.device === 'desktop' && ruffnavInstance.options.desktopHover)) return;
418                // treat Tab and arrow navigation as keyboard navigation
419                const isNavKey = e.key === 'Tab' || e.key === 'Shift' || e.key.startsWith('Arrow');
420                if (isNavKey) {
421                    ruffnavInstance.keyboardMode = true;
422                    // if currently focused element is a nav-back, make it visible
423                    ruffnavInstance.backButtons.forEach(btn => {
424                        try { btn.style.display = 'flex'; } catch (err) { /* ignore */ }
425                    });
426                }
427            };
428
429            // use capture phase to get events early
430            document.addEventListener('pointerdown', this._onPointerDownForModality, true);
431            document.addEventListener('mousedown', this._onPointerDownForModality, true);
432            document.addEventListener('mousemove', this._onPointerDownForModality, true);
433            document.addEventListener('keydown', this._onKeyDownForModality, true);
434        }
435
436        this.subToggler.forEach(subTogglerItem => {
437            if (this.options.desktopHover) {
438                subTogglerItem.parentNode.addEventListener('mouseenter', ruffnavInstance._open);
439                subTogglerItem.parentNode.addEventListener('mouseleave', ruffnavInstance._close);
440            }
441            subTogglerItem.addEventListener('click', ruffnavInstance._open);
442            if (this.options.desktopHover) subTogglerItem.addEventListener('mouseover', ruffnavInstance._open);
443
444            // add keydown listener for Enter key
445            subTogglerItem.addEventListener('keydown', function(e) {
446                if (e.key === 'Enter' || e.key === ' ') { // Also allow Spacebar
447                    e.preventDefault(); // prevent the default action (e.g., click or scrolling for spacebar)
448                    e.stopPropagation(); // stop the event from bubbling up
449                    // find the submenu (ul.sub) associated with this toggler
450                    const submenu = this.nextElementSibling;
451                    if (submenu && submenu.classList.contains('sub')) {
452                        // add the no-transition class before opening
453                        submenu.classList.add('no-transition');
454
455                        // trigger the original open logic using the captured Ruffnav instance
456                        // pass the current event object
457                        ruffnavInstance._open(e);
458                    } 
459                }
460            });
461        });
462        this.backButtons.forEach(btn => {
463            if (this.device === 'desktop' && this.options.desktopHover) btn.style.display = 'none';
464            btn.addEventListener('focus', () => {
465                if (ruffnavInstance.device === 'desktop' && ruffnavInstance.options.desktopHover) {
466                    btn.style.display = ruffnavInstance.keyboardMode ? 'flex' : 'none';
467                }
468            });
469            btn.addEventListener('blur', () => {
470                if (ruffnavInstance.device === 'desktop' && ruffnavInstance.options.desktopHover) {
471                    setTimeout(() => {
472                        if (!btn.parentElement.contains(document.activeElement)) btn.style.display = 'none';
473                    }, 50);
474                }
475            });
476            btn.addEventListener('click', this._close, false);
477            btn.addEventListener('keydown', e => {
478                if (e.key === 'Enter' || e.key === ' ') {
479                    e.preventDefault();
480                    e.stopPropagation();
481                    this._close(e);
482                }
483            });
484        });
485    }
486
487    listenResize = () => {
488        // global resize Event
489        window.onresize = () => {
490            const oldDevice = this.device; // Capture old device type before updating
491            this.device = breakpoint(this.options.mobileBreak) ? 'desktop' : 'mobile';
492
493            // Always update navigation height on resize, especially for desktop
494            if (this.device === 'desktop') {
495                // Ensure height is updated immediately when switching to or resizing on de
495sktop
496                this.navigationHeightUpdater.updateNavigationHeight();
497            }
498
499            if (this.device === 'mobile' || this.options.forceMobileNav) {
500                this.removeListeners();
501                // update aria-attributes
502                this.container.setAttribute('aria-expanded', 'false');
503
504                // add tabindex for all submenus to make them focusable
505                this.sub.forEach(subItem => {
506                    subItem.setAttribute('tabindex', '0');
507                });
508            }
509            if (this.windowSize !== window.innerWidth && this.device === 'desktop') {
510                this.closeRecusive(null, true);
511                this.isOpen = false;
512                // update aria-attributes
513                this.container.removeAttribute('aria-expanded');
514
515                // stop observing focus inside ul
516                document.removeEventListener('keydown', this.initWatchFocus);
517
518                if (this.toggler.checked) this.toggler.checked = false;
519
520                this.sub.forEach(subItem => {
521                    subItem.removeAttribute('tabindex');
522                });
523            }
524
525            this.windowSize = window.innerWidth;
526            // Only call navHeightSubPositioner for submenu positioning logic, which still has a timeout
527            // The navigation height update is now handled directly in the resize listener.
528            
529            // Call the method that contains the debounced submenu positioning logic
530            if (this.sub.length > 0 && this.subMenuPositioner) {
531                this.navHeightSubPositioner();
532            }
533        };
534    }
535
536    scrollTop = () => {
537        this.sub.forEach(item => { item.scrollTop = 0; });
538        this.container.childNodes[0].scrollTop = 0;
539    }
540
541    create = (type, attr, content) => {
542        // create Node element
543        const elm = document.createElement(type);
544        Object.entries(attr).forEach(([name, value]) => {
545            elm.setAttribute(name, value);
546        });
547        if (content) elm.innerHTML = content;
548        return elm;
549    }
550
551    activeClassFallback = (elem) => {
552        if (elem.href === window.location.origin + '/' + this.activePath + this.path[0] + '/') {
553            elem.classList.add('active');
554            elem.setAttribute('aria-current', 'page');
555            if (elem.parentNode.classList.contains(this.options.subParent.substring(1))) elem.parentNode.classList.add('active');
556            if (this.path.length === 1) elem.parentNode.querySelector('[href="' + window.location.origin + '/' + this.activePath + this.path[0] + '/"]').classList.add('active');
557            this.activePath += this.path[0] + '/';
558            this.path.splice(0, 1);
559        }
560    }
561
562    updateNavigationStatus = (text) => {
563        // function to update text inside aria-live element for screenreader
564        const navigationStatusElement = this.navigationStatus;
565        navigationStatusElement.textContent = '';
566        navigationStatusElement.textContent = text;
567    }
568
569    initWatchFocus = (e) => {
570        // call function to observe focus
571        return this.watchFocus(e, this.container.querySelector('.open'));
572    }
573
574    watchFocus = (e, modalElement) => {
575        // observe focus inside ul function
576        const isTabPressed = e.key === 'Tab' || e.keyCode === 9;
577        const isEscPressed = e.key === 'Escape' || e.keyCode === 27;
578
579        if (!isTabPressed && !isEscPressed) return;
580
581        let focusableElements = `:scope > li > [href], :scope > li[role="button"], :scope > li > span[role="button"], :scope > li > span`;
582        let modal = modalElement;
583        if (modal.classList.contains('navi')) {
584            modal = this.container;
585            focusableElements = `.navi > li > [href], .navi > li[role="button"], .navi > li > span[role="button"], .navi > li > span, .btn, button, [tabindex="0"], [href], a.branding, .phone, .email, .mobile-close-button`;
586        }
587
588        // get focusable and visible elements in modal
589        const visibleElements = [];
590        const focusableContent = modal.querySelectorAll(focusableElements);
591
592        focusableContent.forEach(thisElement => {
593            if (thisElement.checkVisibility()) visibleElements.push(thisElement);
594        });
595
596        const firstFocusableElement = visibleElements[0];
597        const lastFocusableElement = visibleElements[visibleElements.length - 1];
598
599        if (isTabPressed) {
600            if (e.shiftKey) {
601                if (document.activeElement === firstFocusableElement) {
602                    // if first element is focused and shift + tab is clicked, close menu
603                    e.preventDefault();
604                    this.focusCloseAction(e, modal);
605                } else if (document.activeElement === modal) {
606                    // if menu is focused and shift + tab is clicked, close focused menu
607                    e.preventDefault();
608                    this.focusCloseAction(e, modal);
609                }
610            } else if (!e.shiftKey && document.activeElement === lastFocusableElement) {
611                // if last element is focused and tab is clicked, close menu
612                e.preventDefault();
613                this.focusCloseAction(e, modal);
614            }
615        } else if (isEscPressed) {
616            // if Esc is clicked, close current menu
617            e.preventDefault();
618            this.focusCloseAction(e, modal);
619        }
620    }
621
622    focusCloseAction = (e, modal) => {
623        // handle focus on close
624        if (modal === this.container) {
625            // if first-level, close navigation and set focus to burger icon
626            this.toggler.checked = false;
627            this._closeAll(e);
628            const toggleLabel = document.querySelector(this.options.toggleLabel);
629            toggleLabel.focus();
630        } else {
631            // if not first-level, toggle nav-back and close submenu
632            modal.querySelector(':scope > .nav-back').click();
633        }
634    }
635
636    navHeightSubPositioner = () => {
637        // Debounced logic for submenu positioning on resize
638        if (this.viewportTimer) clearTimeout(this.viewportTimer); // Clear previous timeout
639
640        this.viewportTimer = setTimeout(() => {
641            // Use requestAnimationFrame to ensure layout is updated before measuring
642            requestAnimationFrame(() => {
643                if (breakpoint(this.options.mobileBreak) && this.sub.length > 0 && this.subMenuPositioner) {
644                    // Desktop: Trigger submenu repositioning via the already initialized SubmenuPositioner
645                    // The MutationObserver in SubmenuPositioner handles opening/closing.
646                    // This part handles resize.
647                    // Re-adjust position for all currently open submenus on resize
648                    this.container.querySelectorAll('ul.sub.open').forEach(openSubmenu => {
649                        if (!this.subMenuPositioner.shouldExclude(openSubmenu)) {
650                            // Reset first, then adjust. Resetting position clears previous transform/top styles.
651                            this.subMenuPositioner.resetPositioning(openSubmenu);
652                            this.subMenuPositioner.adjustPosition(openSubmenu);
653                        }
654                    });
655                } else if (this.sub.length > 0 && this.subMenuPositioner) {
656                    // Mobile: Destroy submenu positioning logic and reset inline styles if it was active
657                    // Only destroy if SubmenuPositioner was actually initialized
658                    this.subMenuPositioner.destroy(); // Stop submenu adjustments and remove resize listener
659
660                    // Reset any inline styles added by the submenu positioner on all submenus
661                    this.sub.forEach(submenu => {
662                        this.subMenuPositioner.resetPositioning(submenu); // Use SubmenuPositioner method to reset
663                    });
664                }
665                // Clear the subMenuPositioner instance on mobile to allow re-initialization on desktop switch
666                if (this.device === 'mobile' || this.options.forceMobileNav) {
667                    this.subMenuPositioner = null; // Clear instance
668                }
669            }); // End of requestAnimationFrame
670
671        }, 750); // Increased delay to give layout more time to settle after resize
672    }
673}
674
675// Polyfills for Object.entries and Element.remove
676if (!Object.entries) {
677    Object.entries = function (obj) {
678        const ownProps = Object.keys(obj);
679        const i = ownProps.length;
680        const resArray = new Array(i); // preallocate the Array
681        while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]];
682        return resArray;
683    };
684}
685if (!('remove' in Element.prototype)) {
686    Element.prototype.remove = function () {
687        if (this.parentNode) this.parentNode.removeChild(this);
688    };
689}
690
691// Class to update and manage the navigation height as a CSS variable
692class NavigationHeightUpdater {
693    constructor(navigationContainer = document.getElementById('navigation')) {
694        this.classNamesToMonitor = ['font-size-110', 'font-size-130']; // Classes to monitor for changes
695        this.navigationContainer = navigationContainer; // navigation element with class from ruffnav
696        this.updateNavigationHeight();  // Perform initial height update
697        this.startMonitoring();  // Monitor <html> class changes
698        this.observeHeightChanges(); // Start observing height changes for dynamic navigation-height-dynamic variable
699    }
700
701    // Method to calculate and update the navigation height
702    updateNavigationHeight = () => {
703        const navigationHeight = this.navigationContainer.offsetHeight;
704        document.documentElement.style.setProperty('--navigation-height', `${navigationHeight}`);
705    }
706
707    // Observe changes in the navigation container's height and update --navigation-height-dynamic
708    observeHeightChanges = () => {
709        let lastHeight = this.navigationContainer.offsetHeight;
710        // Set initial value
711        document.documentElement.style.setProperty('--navigation-height-dynamic', `${lastHeight}`);
712
713        // Use ResizeObserver to detect any height changes (including those caused by CSS)
714        const resizeObserver = new ResizeObserver(entries => {
715            for (let entry of entries) {
716                const newHeight = entry.target.offsetHeight;
717                if (newHeight !== lastHeight) {
718                    document.documentElement.style.setProperty('--navigation-height-dynamic', `${newHeight}`);
719                    lastHeight = newHeight;
720                }
721            }
722        });
723        resizeObserver.observe(this.navigationContainer);
724    }
725
726    // Set up monitoring for class changes on <html>
727    startMonitoring = () => {
728        const htmlElement = document.documentElement;
729
730        // Observer to track changes in <html> element's class attribute
731        const observer = new MutationObserver((mutations) => {
732            mutations.forEach((mutation) => {
733                if (mutation.attributeName === 'class') {
734                    const classFound = this.classNamesToMonitor.some(className => 
735                        htmlElement.classList.contains(className));
736
737                    // Update the height if any of the monitored classes are found
738                    if (classFound || !classFound) this.updateNavigationHeight(); // Trigger immediate height update
739                }
740            });
741        });
742
743        // Observe for attribute changes on <html>
744        observer.observe(htmlElement, { attributes: true });
745    }
746}
747
748// Class to manage submenu positioning and prevent it from leaving the viewport
749class SubmenuPositioner {
750    constructor(closeButtonWidth = 30, excludedSubmenus = [], submenus = document.getElementById('navigation').querySelectorAll('ul.sub'), ruffnavInstance) {
751        this.closeButtonWidth = closeButtonWidth; // Width of the close button in submenus
752        this.excludedSubmenus = excludedSubmenus; // Submenus to be excluded from repositioning
753        this.submenus = submenus; // all submenus with class from ruffnav
754        this.ruffnavInstance = ruffnavInstance; // Store Ruffnav instance
755        this.init(); // Initialize the observer
756    }
757
758    // Initialize the MutationObserver and event listener
759    init() {
760        // Observer to track when submenus are opened (i.e., when they gain the "open" class)
761        this.observer = new MutationObserver((mutations) => {
762            mutations.forEach((mutation) => {
763                // Check if the target is a submenu and gained the 'open' class or lost the 'open' class (for reset)
764                if (mutation.target.classList && mutation.target.classList.contains('sub')) {
765                    if (mutation.target.classList.contains('open')) {
766                        const submenu = mutation.target;
767                        const oldClasses = mutation.attributeOldValue ? mutation.attributeOldValue.split(' ') : [];
768                        const wasCurrentBefore = oldClasses.includes('current');
769                        const isNowOpen = submenu.classList.contains('open'); // Redundant check, but for clarity
770                        // Only call adjustPosition if the submenu is gaining the 'open' class
771                        // AND it was NOT current before.
772                        // This prevents re-positioning when clicking a parent toggler while a child is open.
773                        if (isNowOpen && !wasCurrentBefore) {
774                            if (!this.shouldExclude(submenu) && !( this.ruffnavInstance.device === 'mobile' || this.ruffnavInstance.options.forceMobileNav)) {
775                                this.adjustPosition(submenu);
776                             } // Reposition submenu if not excluded
777                        }
778                    } else { // If it lost the 'open' class, check if it's a genuine close (lost 'current' as well)
779                        const submenu = mutation.target;
780                        // Only reset if the submenu was truly closed (lost 'open' AND 'current')
781                        if (!submenu.classList.contains('open') && !submenu.classList.contains('current')) {
782                            this.resetPositioning(submenu); // Reset positioning
783                        }
784                    }
785                }
786            });
787        });
788
789        // Observe all submenus for class changes
790        this.submenus.forEach(submenu => {
791            this.observer.observe(submenu, { attributes: true, attributeFilter: ['class'] });
792        });
793    }
794
795    // Cleanup: Disconnect observer
796    destroy = () => {
797        if (this.observer) this.observer.disconnect(); // Stop observing submenu changes
798    }
799
800    // Helper method to determine if a submenu should be excluded from repositioning
801    shouldExclude = (submenu) => {
802        const parentName = submenu.getAttribute('data-parent-name');
803        return this.excludedSubmenus.includes(parentName); // Return true if it should be excluded
804    }
805
806    // Reset positioning styles for a submenu
807    resetPositioning = (submenu) => {
808        submenu.dataset.positioned = "false"; // Mark submenu as not positioned
809        
810        // Only reset transform if the submenu is truly closed (has neither open nor current class)
811        if (!submenu.classList.contains('open') && !submenu.classList.contains('current')) {
812            submenu.style.transform = '';         // Clear any transformation
813        }
814
815        submenu.style.top = '';               // Reset top position
816        submenu.style.bottom = '';            // Reset bottom position
817        submenu.style.left = '';              // Reset left position
818        submenu.style.right = '';             // Reset right position
819    }
820
821    // Adjust the position of the submenu to ensure it stays within the viewport
822    adjustPosition = (submenu) => {
823        if (submenu.dataset.positioned === "true") return; // Skip if already positioned
824
825        const submenuRect = submenu.getBoundingClientRect();
826        const viewportHeight = window.innerHeight;
827        const viewportWidth = window.innerWidth;
828        const leftMargin = 25; // Margin to leave from the left edge of the screen
829
830        // Vertical adjustments: Ensure the submenu stays within the viewport vertically
831        if (submenuRect.bottom > viewportHeight) {
832            submenu.style.top = `${viewportHeight - submenuRect.bottom}px`;
833        } else if (submenuRect.top < 0) {
834            submenu.style.top = `${-submenuRect.top}px`;
835        }
836
837        // Horizontal adjustments: Ensure the submenu stays within the viewport horizontally
838        let translateXValue = 0;
839        if (submenuRect.right + this.closeButtonWidth > viewportWidth) {
840            const overflowRight = submenuRect.right + this.closeButtonWidth - viewportWidth;
841            translateXValue = -overflowRight*2; // Shift the submenu left if overflowing on the right
842        }
843
844        if (submenuRect.left + translateXValue < leftMargin) {
845            const overflowLeft = leftMargin - (submenuRect.left + translateXValue);
846            translateXValue += overflowLeft; // Shift the submenu right if overflowing on the left
847        }
848
849        // Always apply the calculated transform value, even if it's 0
850        submenu.style.transform = `translateX(${translateXValue}px)`;
851
852        submenu.dataset.positioned = "true"; // Mark submenu as positioned to avoid redundant adjustments
853    }
854}

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.