PageSourceSearch

https://www.malerei-klood.de/js/_essentials/ruffnav.js

js malerei-klood.de collected 2026-10-06 16:26:29 UTC 24,013 bytes, 521 lines download raw bytes

1/**
2 * off-canvas nav
3 * @name ruffnav
4 * @author Dominik Kressler
5 * @version 2.4.3
6 * @thanks Jasmin Reiff
7 * @edit by Malte Zoudlik
8 * @edit by Dario Rieke
9 * @edit by Marc-Kevin Scharlipp
10 * @lastedit 2024-09-30
11 * @copyright (c) 2018
12 */
13function Ruffnav(options) {
14    this.options = {
15        toggler: '#navi-toggled',
16        container: '#navigation',
17        navList: '.navi-main',
18        subParent: '.has-sub',
19        subClass: '.sub',
20        backText: false,
21        mobileBreak: 'large',
22        closeOnScroll: true,
23        desktopHover: false,
24        navSplit: false,
25        navAppend: false
26    }
27    this.options = { ...this.options, ...options }
28
29    this.depth = 0;
30    this.toggler = document.querySelector(this.options.toggler);
31    this.container = document.querySelector(this.options.container);
32    this.navList = this.container.querySelector(this.options.navList)
33    this.sub = this.container.querySelectorAll(this.options.subClass);
34    this.subParent = this.container.querySelectorAll(this.options.subParent);
35    this.html = document.documentElement;
36    this.device = breakpoint(this.options.mobileBreak) ? 'desktop' : 'mobile';
37    this.isOpen = false;
38    this.subToggler = null;
39    this.windowSize = window.innerWidth;
40    this.navSize = 0;
41    this.activePath = '';
42    this.path = window.location.pathname.split('/').filter(function (el) { return el != '' });
43    this.navigationStatus = document.querySelector('[data-navigation-status]');
44    this.lang = this.html.lang.split(/[-_]/)[0].toLowerCase();
45
46    // true if nav is opened first time or reopened
47    var alreadyOpened = false;
48
49    // set back button text by language, or by overwrite
50    if (this.options.backText === false) {
51        this.options.backText = (this.lang == 'de') ? 'Zurück' : 'back';
52    }
53
54    this.backButton = create('li', {
55        'class': 'nav-back',
56        'tabindex': '0',
57        'aria-label': this.options.backText,
58        'role': 'button'
59    }, '<span>' + this.options.backText + '</span>');
60
61    var self = this;
62    this.init = function () {
63        if (!self.navList) return console.warn(`No element found with class "${self.options.navList}". Ruffnav could't be initialized. Please add the class to your main navigation UL-Element`);
64        // add class for desktop hover
65        self.container.classList.add('first-level');
66        if (self.options.desktopHover) self.container.classList.add('hover-navigation');
67
68        const navigationName = (self.lang == 'de') ? `Hauptnavigation` : `main navigation`
69        self.container.firstElementChild.setAttribute('aria-label', navigationName);
70
71        // remove aria-expanded attribute on desktop
72        const naviUl = self.navList.querySelector(':scope > .navi');
73        let deviceType = breakpoint(self.options.mobileBreak) ? 'desktop' : 'mobile';
74
75        // remove expanded attribute for desktop navigation
76        if (deviceType === 'desktop') {
77            self.container.removeAttribute('aria-expanded');
78        }
79
80        // append custom Content to Navigation
81        if (self.options.navAppend) {
82            var itemToAppend = create('li', { 'class': 'nav-item appended-item' }, self.options.navAppend);
83            self.navList.appendChild(itemToAppend);
84        }
85
86        self.navSize = self.navList.childNodes.length;
87        // split navigation in half
88
89        // transform Navigation markup
90        [].forEach.call(self.subParent, function (parentItem) {
91            var childNode = parentItem.firstElementChild,
92                categoryLabel = (childNode.dataset && childNode.dataset.categoryLabel) ? childNode.dataset.categoryLabel : childNode.innerHTML.replace(/(<([^>]+)>)/gi, "");
93
94            // parent-name attribute for UL's
95            let subParentName = childNode.textContent;
96            subParentName = subParentName.replace(/(\r\n|\n|\r)/gm, '');
97            childNode.nextElementSibling.setAttribute('data-parent-name', subParentName.trim());
98
99            // only clone when Element is a link
100            if (childNode.nodeName !== 'SPAN') {
101                let activeLinkClass = (childNode.classList.contains('active') && window.location.origin+window.location.pathname === childNode.href) ? 'active' : '';
102
103                // copy link inside UL's
104                childNode.nextElementSibling.insertBefore(create('li', {
105                    'class': 'nav-item ' + childNode.className.trim()
106                }, '<a href="' + childNode.href + '" title="' + childNode.title + '" class="'+ activeLinkClass +'">' + categoryLabel + '</a>'), parentItem.children[1].children[0]);
107                if (self.path.length) {
108                    self.activeClassFallback(childNode);
109                }
110            }
111
112            const subTogglerLabel = (self.lang == 'de') ? `${categoryLabel} Unterverzeichnis öffnen` : `open ${categoryLabel} sub-navigation`;
113            // replace link's with span's
114            parentItem.replaceChild(create('span', {
115                'class': 'sub-toggler ' + childNode.className.trim(),
116                'tabindex': 0,
117                'data-href': childNode.href,
118                'aria-haspopup': 'true',
119                'aria-expanded': 'false',
120                'aria-label': subTogglerLabel,
121                'role': 'button'
122            }, childNode.innerHTML), childNode)
123        });
124
125        // add backbuttons for all submenus
126        [].forEach.call(self.sub, function (subItem) {
127            subItem.setAttribute('aria-expanded', 'false');
128            const subMenuLabel = (self.lang == 'de') ? `${subItem.dataset.parentName} Unterverzeichnis` : `${subItem.dataset.parentName} sub-navigation`;
129            subItem.setAttribute('aria-label', subMenuLabel);
130
131            // add tabindex for mobile navigation to make submenu focusable
132            if (deviceType === 'desktop') {
133                subItem.removeAttribute('tabindex');
134            } else if (deviceType === 'mobile') {
135                subItem.setAttribute('tabindex', '0');
136            }
137
138            // add backbuttons to UL's and add aria-label 
139            const thisBackButton = self.backButton.cloneNode(true);
140            const backButtonLabel = (self.lang == 'de') ? `${subItem.dataset.parentName}
140 Unterverzeichnis schließen` : `close ${subItem.dataset.parentName} sub-navigation`;
141            thisBackButton.setAttribute('aria-label', backButtonLabel);
142            subItem.insertBefore(thisBackButton, subItem.childNodes[0]);
143        });
144
145        // navigation split
146        if (self.options.navSplit) {
147            var elm;
148            var splitList = create('ul', { class: 'navi split-list' });
149            for (var i = parseInt(self.navSize / 2); i != splitList.childNodes.length; i) {
150                elm = self.navList.childNodes[i].cloneNode(true);
151                splitList.appendChild(elm);
152                self.navList.childNodes[i].remove();
153            }
154            self.navList.parentElement.appendChild(splitList);
155        }
156
157        // add aria attributes to each link inside navigation
158        let allNavigationLinks = self.navList.querySelectorAll('a');
159        allNavigationLinks.forEach(naviLink => {
160            // update / set aria-attributes for current location
161            if(naviLink.classList.contains('active')) {
162                let naviLinkName = naviLink.textContent;
163                naviLinkName = naviLinkName.replace(/(\r\n|\n|\r)/gm, '');
164                const activePageLabel = (self.lang == 'de') ? `${naviLinkName} (aktuelle Seite)` : `${naviLinkName} (current location)`;
165                naviLink.setAttribute('aria-label', activePageLabel);
166            } else if(naviLink.getAttribute('target') === '_blank') {
167                let naviLinkName = naviLink.textContent;
168                naviLinkName = naviLinkName.replace(/(\r\n|\n|\r)/gm, '');
169                const externPageLabel = (self.lang == 'de') ? `${naviLinkName} (öffnet eine neue Seite)` : `${naviLinkName} (opens a new window)`;
170                naviLink.setAttribute('aria-label', externPageLabel);
171            }
172        });
173
174        // close on mobile toggle
175        self.toggler.addEventListener('click', self._closeAll);
176        // add Listeners for different events
177        self.addListener();
178        // add global resize listener
179        self.listenResize();
180
181        return self;
182    }
183    this._open = function (e) {
184        // open event
185        var target, sameLevelCurrents;
186
187        if (e.type === 'mouseenter' && self.device === 'mobile') return;
188        if (e.type === 'mouseenter') {
189            target = e.target;
190            target.childNodes[1].classList.add('current');
191            target.childNodes[1].setAttribute('aria-expanded', 'true');
192            if (self.container.querySelector('.open') != null) self.container.querySelector('.open').classList.remove('open');
193            target.childNodes[1].classList.add('open');
194
195            if (self.options.desktopHover) {
196                target.querySelector(':scope > .sub').setAttribute('aria-expanded', 'true');
197                target.querySelector(':scope > .sub').classList.add('current');
198            }
199        } else {
200            target = e.currentTarget;
201
202            //close other on same level when already opened
203            sameLevelCurrents = target.parentElement.parentElement.querySelector('.current');
204            if (sameLevelCurrents !== null) self.closeRecusive(sameLevelCurrents, false);
205
206            target.nextElementSibling.classList.add('current');
207            target.setAttribute('aria-expanded', 'true');
208            target.nextElementSibling.setAttribute('aria-expanded', 'true');
209            if (self.container.querySelector('.open') != null) self.container.querySelector('.open').classList.remove('open');
210            target.nextElementSibling.classList.add('open');
211
212            // inform screenreader via aria-live that submenu got opened
213            const currentSubName = target.nextElementSibling.getAttribute('aria-label');
214            const subStatusOpened = (self.lang == 'de') ? `${currentSubName} wurde geöffnet` : `${currentSubName} got opened`;
215            updateNavigationStatus('');
216            updateNavigationStatus(subStatusOpened);
217
218            // set focus on submenu
219            setTimeout(() => {
220                target.nextElementSibling.focus()
221            }, '250');
222
223            // oberserve focus inside submenu
224            document.addEventListener('keydown', initWatchFocus);
225        }
226        if (!self.isOpen && self.device !== 'mobile') {
227            if (self.options.closeOnScroll) window.addEventListener('scroll', self._closeAll);
228            document.addEventListener('click', self._closeAll);
229            // document.addEventListener('keydown', self._closeAll);
230            self.isOpen = true;
231        }
232        self.container.classList.remove('first-level');
233        self.scrollTop();
234    }
235    this._close = function (e) {
236        // close event
237        var currentListTree; 
238        if (e.type === 'mouseleave' && self.device === 'mobile') return;
239        if (e.type === 'mouseleave') currentListTree = e.currentTarget;
240        else currentListTree = e.currentTarget.parentElement;
241
242        self.closeRecusive(currentListTree, false);
243        if (self.container.querySelectorAll('ul.current').length === 0) self.isOpen = false;
244        if (self.container.querySelectorAll('ul.current').length === 0) self.container.classList.add('first-level');
245
246        // inform screenreader via aria-live that submenu got closed
247        const currentSubName = currentListTree.getAttribute('aria-label');
248        const subStatusClosed = (self.lang == 'de') ? `${currentSubName} wurde geschlossen` : `${currentSubName} got closed`;
249        updateNavigationStatus('');
250        updateNavigationStatus(subStatusClosed);
251
252        // set focus for mobile devices
253        const thisSubToggler = currentListTree.parentElement.firstElementChild;
254        thisSubToggler.focus();
255
256        target = e.currentTarget;
257        if (self.options.desktopHover && e.type === 'mouseleave') {
258            target.querySelector(':scope > .sub').setAttribute('aria-expanded', 'false');
259        }
260
261        self.scrollTop();
262    }
263    this._closeAll = function (e) {
264        // check for toggle input and update attributes
265        (self.toggler.checked) ? self.html.classList.add('hidden-scroll') : self.html.classList.remove('hidden-scroll');
266        (self.toggler.checked) ? self.container.setAttribute('aria-expanded', 'true') : self.container.setAttribute('aria-expanded', 'false');
267        (self.toggler.checked) ? '' : alreadyOpened = false;
268        const toggleLabel = document.querySelector('label[for="navi-toggled"] [role="button"]');
269        const menuOpenTogglerLabel = (self.lang == 'de') ? `Hauptnavigation öffnen` : `open main navigation`;
270        const menuCloseTogglerLabel = (self.lang == 'de') ? `Hauptnavigation schließen` : `close main navigation`;
271        (self.toggler.checked) ? toggleLabel.setAttribute('aria-label', menuCloseTogglerLabel) : toggleLabel.setAttribute('aria-label', menuOpenTogglerLabel);
272        (self.toggler.checked) ? toggleLabel.setAttribute('aria-expanded', 'true') : toggleLabel.setAttribute('aria-expanded', 'false');
273
274        // inform screenreader via aria-live if navigation got closed or opened on mobile 
275        if(self.device === 'mobile') {
276            const navigationStatusOpened = (self.lang == 'de') ? `Hauptnavigation wurde geöffnet` : `Main navigation got opened`;
277            const navigationStatusClosed = (self.lang == 'de') ? `Hauptnavigation wurde geschlossen` : `Main navigation got closed`;
278            updateNavigationStatus('');
279            (self.toggler.checked) ? updateNavigationStatus(navigationStatusOpened) : updateNavigationStatus(navigationStatusClosed);
280        }
281
282        // close all
283        if (e.type === 'scroll') {
284            self.closeRecusive(null, true);
285        } else if (e.type === 'click') {
286            // check if click was not inside navigation
287            if (e.target === self.container || self.container.contains(e.target)) return;
288            else self.closeRecusive(null, true)
289        } else if (e.type === 'keydown') {
290            // check if "enter" was pressed
291            if (e.keyCode === 27) self.closeRecusive(null, true);
292        } else {
293            self.closeRecusive(null, true);
294        }
295        
296        self.removeListeners();
297        self.isOpen = false;
298    }
299    this.closeRecusive = function (firstList, closeAll) {
300        // close all open items
301        if (closeAll) {
302            [].forEach.call(self.container.querySelectorAll('.current'), function (openSubs) { 
303                openSubs.classList.remove('current');
304                openSubs.setAttribute('aria-expanded', 'false');
305                openSubs.parentNode.firstElementChild.setAttribute('aria-expanded', 'false');
306            });
307            if (self.container.querySelector('.open') != null) self.container.querySelector('.open').classList.remove('open');
308
309            self.container.classList.add('first-level');
310        } else {
311            if (self.container.querySelector('.open') != null) self.container.querySelector('.open').classList.remove('open');
312
313            [].forEach.call(firstList.querySelectorAll('.current'), function (openSubs) {
314                openSubs.classList.remove('current');
315                openSubs.setAttribute('aria-expanded', 'false');
316                openSubs.parentNode.firstElementChild.setAttribute('aria-expanded', 'false');
317
318                // stop observing focus inside ul
319                if(self.device === 'mobile') {
320                    document.removeEventListener('keydown', initWatchFocus);
321                }
322            });
323
324            // update classes and attributes
325            firstList.parentNode.parentNode.classList.add('open');
326            firstList.classList.remove('current');
327            firstList.setAttribute('aria-expanded', 'false');
328            firstList.parentNode.firstElementChild.setAttribute('aria-expanded', 'false');
329        }
330
331        // if navigation is opened initially or reopened, set focus and oberserve focus inside submenu
332        if(self.toggler.checked && self.container.querySelectorAll('ul.current').length === 0 && self.device == 'mobile' && !alreadyOpened) {
333            const naviUl = self.navList.querySelector('ul.navi');
334
335            if(naviUl) {
336                naviUl.classList.add('open');
337            } else if(self.navList.classList.contains('navi')) {
338                self.navList.classList.add('open');
339            }
340            
341            setTimeout(() => {
342                // oberserve focus inside submenu
343                document.addEventListener('keydown', initWatchFocus);
344            }, '300');
345
346            alreadyOpened = true;
347        }
348    }
349    this.removeListeners = function () {
350        // remove eventListener for global close
351        window.removeEventListener('scroll', self._closeAll);
352        document.removeEventListener('keydown', self._closeAll);
353        document.removeEventListener('click', self._closeAll);
354        document.removeEventListener('keydown', initWatchFocus);
355    }
356    this.addListener = function () {
357        // add eventListener to sub-togglers and back-buttons
358        self.subToggler = self.container.querySelectorAll('.sub-toggler');
359        self.backButtons = self.container.querySelectorAll('.nav-back');
360        [].forEach.call(self.subToggler, function (subTogglerItem) {
361            if (self.options.desktopHover) {
362                subTogglerItem.parentNode.addEventListener('mouseenter', self._open);
363                subTogglerItem.parentNode.addEventListener('mouseleave', self._close);
364            }
365            subTogglerItem.addEventListener('click', self._open);
366        });
367        [].forEach.call(self.backButtons, function (backButtonsItem) {
368            backButtonsItem.addEventListener('click', self._close, false);
369        });
370    }
371    this.listenResize = function () {
372        // global resize Event
373        window.onresize = function () {
374            self.device = breakpoint(self.options.mobileBreak) ? 'desktop' : 'mobile';
375            if (self.device === 'mobile') {
376                self.removeListeners()
377                // update aria-attributes
378                self.container.setAttribute('aria-expanded', 'false');
379
380                // add tabindex for all submenus to make them focusable
381                [].forEach.call(self.sub, function (subItem) {
382                    subItem.setAttribute('tabindex', '0');
383                });
384            }
385            if (self.windowSize != window.innerWidth && self.device === 'desktop') {
386                self.closeRecusive(null, true);
387                self.isOpen = false;
388                // update aria-attributes
389                self.container.removeAttribute('aria-expanded');
390                
391                // stop observing focus inside ul
392                document.removeEventListener('keydown', initWatchFocus);
393
394                if(self.toggler.checked) {
395                    self.toggler.checked = false;
396                }
397
398                [].forEach.call(self.sub, function (subItem) {
399                    subItem.removeAttribute('tabindex');
400                });
401            }
402            self.windowSize = window.innerWidth;
403        }
404    }
405
406    this.scrollTop = function () {
407        [].forEach.call(self.sub, function (item) { item.scrollTop = 0 })
408        self.container.childNodes[0].scrollTop = 0;
409    }
410
411    function create(type, attr, content) {
412        // create Node element
413        var elm = document.createElement(type);
414        [].forEach.call(Object.entries(attr), function (name) {
415            elm.setAttribute(name[0], name[1]);
416        });
417        if (content) elm.innerHTML = content;
418        return elm;
419    }
420
421    this.activeClassFallback = function (elem) {
422        if (elem.href === window.location.origin + '/' + this.activePath + this.path[0] + '/') {
423            elem.classList.add('active');
424            if (elem.parentNode.classList.contains(self.options.subParent.substring(1))) elem.parentNode.classList.add('active')
425            if (this.path.length === 1) elem.parentNode.querySelector('[href="' + window.location.origin + '/' + this.activePath + this.path[0] + '/"]').classList.add('active');
426            this.activePath += this.path[0] + '/';
427            this.path.splice(0, 1);
428        }
429    }
430
431    // function to update text inside aria-live element for screenreader
432    function updateNavigationStatus(text) {
433        const navigationStatusElement = self.navigationStatus;
434        navigationStatusElement.textContent = text;
435    }
436
437    // call function to observe focus
438    function initWatchFocus(e) {
439        return watchFocus(e, self.container.querySelector('.open'));
440    }
441
442    // observe focus inside ul function
443    function watchFocus(e, modalElement) {
444        const isTabPressed = e.key === 'Tab' || e.keyCode === 9;
445        const isEscPressed = e.key === 'Escape' || e.keyCode === 27;
446
447        if (!isTabPressed && !isEscPressed) return;
448
449        let focusableElements = `:scope > li > [href], :scope > li[role="button"], :scope > li > span[role="button"], :scope > li > span`;
450        let modal = modalElement; 
451        if(modal.classList.contains('navi')) {
452            modal = document.getElementById('navigation');
453            focusableElements = `.navi > li > [href], .navi > li[role="button"], .navi > li > span[role="button"], .navi > li > span, button, a.branding, .phone, .email, .mobile-close-button`;
454        }
455
456        // get focusable and visible elements in modal
457        let visibleElements = [];
458        const focusableContent = modal.querySelectorAll(focusableElements);
459
460        focusableContent.forEach(thisElement => {
461            if(thisElement.checkVisibility()) {
462                visibleElements.push(thisElement);
463            }
464        });
465
466        const firstFocusableElement = visibleElements[0];
467        const lastFocusableElement = visibleElements[visibleElements.length - 1];
468
469        if (isTabPressed) {
470            if (e.shiftKey) {
471                if (document.activeElement === firstFocusableElement) {
472                    // if first element is focused and shift + tab is clicked, close menu
473                    e.preventDefault();
474                    focusCloseAction(e, modal);
475                } else if (document.activeElement === modal) {
476                    // if menu is focused and shift + tab is clicked, close focused menu
477                    e.preventDefault();
478                    focusCloseAction(e, modal);
479                }
480            } else if (document.activeElement === lastFocusableElement) {
481                // if last element is focused and tab is clicked, close menu
482                e.preventDefault();
483                focusCloseAction(e, modal);
484            }
485        } else if (isEscPressed) {
486            // if Esc is clicked, close current menu
487            e.preventDefault();
488            focusCloseAction(e, modal);
489        }
490    }
491
492    function focusCloseAction(e, modal) {
493        if (modal === document.getElementById('navigation')) {
494            // if first-level, close navigation and set focus to burger icon
495            self.toggler.checked = false;
496            self._closeAll(e);
497            const toggleLabel = document.querySelector('label[for="navi-toggled"] [role="button"]');
498            toggleLabel.focus();
499        } else {
500            // if not first-level, toggle nav-back and close submenu
501            modal.querySelector(':scope > .nav-back').click();
502        }
503    }
504
505    this.init();
506}
507if (!Object.entries)
508    Object.entries = function (obj) {
509        var ownProps = Object.keys(obj),
510            i = ownProps.length,
511            resArray = new Array(i); // preallocate the Array
512        while (i--)
513            resArray[i] = [ownProps[i], obj[ownProps[i]]];
514
515        return resArray;
516    };
517if (!('remove' in Element.prototype)) {
518    Element.prototype.remove = function () {
519        if (this.parentNode) this.parentNode.removeChild(this);
520    };
521}

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.