PageSourceSearch

https://stm.fi/o/common-ui-resources/standalone/mobile-navigation/…=chrome&languageId=sv_SE&t=1790003306000

js stm.fi collected 2026-09-24 17:41:56 UTC 12,202 bytes, 304 lines download raw bytes

1//
2// YJA Mobile Navigation
3//
4
5// Toggle open / close
6function yjaMobileNavigation(yjaMobileNavConfig) {
7
8    let yjaMenuToggler = [] || "";
9    let yjaMenuMainToggler = "mmenu-open-panel";
10    const yjaMenuWrapper = document.querySelector('.yja-mobile-navigation');
11    const yjaMenuButtonClose = document.getElementById("yjaMenuButtonClose");
12    const yjaMenuButtonLast = document.getElementById('yjaMenuBottomButton');
13    const toggleChildMenuButtons = document.querySelectorAll('button.yja-mobile-navigation__button--toggler');
14
15
16    let config = {
17        navItemId: 'mainMenubar',
18        togglerElement: '',
19        yjaMenuMainToggler: 'mmenu-open-panel',
20        elementsToHide: [] || "",
21        excludedElements: ['#skip-to-content','#footer'],
22        focusableElements: ['a[href]','area[href]','button','details','input','iframe','select','textarea','[contentEditable=""]','[contentEditable="true"]','[contentEditable="TRUE"]','[tabindex]:not([tabindex^="-"])'] //':not([disabled])'
23    }
24
25    document.addEventListener('DOMContentLoaded', () => {
26        config = { ...config, ...yjaMobileNavConfig };
27        yjaMenuMainToggler = document.getElementById(config.yjaMenuMainToggler);
28
29        if (config.togglerElement === '') config.togglerElement =  `#${config.yjaMenuMainToggler}`;
30
31
32
33        if ( config.togglerElement !== '' && yjaMenuWrapper !== null) {
34            if (config.togglerElement.startsWith('.')) yjaMenuToggler = [...document.querySelectorAll(config.togglerElement)];
35            else yjaMenuToggler = document.querySelector(config.togglerElement);
36
37
38            // Open mobile menu
39            if (Array.isArray(yjaMenuToggler)) {
40                yjaMenuToggler.forEach(toggler => {
41                    toggler.addEventListener("click", (evt) => {
42                        evt.preventDefault()
43                        openMobileNavigation(toggler, yjaMenuWrapper, config)
44                    }, false);
45                })
46            } else {
47                yjaMenuToggler.addEventListener("click", (evt) => {
48                    evt.preventDefault()
49                    openMobileNavigation(yjaMenuToggler, yjaMenuWrapper, config)
50                }, false);
51
52            }
53
54
55
56            if (yjaMenuButtonClose !== null) {
57                // Close mobile menu by clock
58                yjaMenuButtonClose.addEventListener("click", () => {
59                    // Hide other DOM elements from screen readers
60                    modifyDomElements(config,true);
61                    closeMobileMenu(yjaMenuWrapper, yjaMenuToggler);
62
63
64                }, false);
65
66                // If user press shift + TAB, jump to last focusable element on navigation
67                yjaMenuButtonClose.addEventListener("keydown", (evt) => {
68                    const focusable = focusables(yjaMenuWrapper);
69                    if (evt.shiftKey && evt.key === 'Tab') {
70                        if (document.activeElement === focusable[0]) {
71                            // Find first nav level links nav links/buttons without LI elements
72                            const mainLinks = [...focusable].filter(item => {
73                                if (item.closest('li') !== null) {
74                                    if (item.closest('li').classList.contains('level-1')) {
75                                        return item;
76                                    }
77                                } else {
78                                    return item;
79                                }
80                            });
81
82                            if (mainLinks.length > 0) {
83                                // Move to last element
84                                mainLinks[mainLinks.length - 1].focus();
85                            }
86                        }
87                    }
88                }, false);
89
90            }
91
92            // Toggle child menu visibility
93            if (toggleChildMenuButtons.length > 0) {
94                toggleChildMenuButtons.forEach(button => {
95                    button.addEventListener('click', () => {
96
97                        // find closest div
98                        const parent = button.closest('div');
99                        // find closest LI
100                        const li = parent.closest('li');
101                        // find next UL that contains child elements
102                        const ul = parent.nextElementSibling;
103
104                        // if UL exists toggle its visibility
105                        if (ul !== null) {
106
107                            button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === 'false' ? 'true' : 'false');
108                            if (button.getAttribute('aria-expanded') === 'false') {
109                                li.classList.remove('yja-mobile-navigation--open');
110                            } else {
111                                li.classList.add('yja-mobile-navigation--open');
112                            }
113                        }
114                    });
115                });
116            }
117
118
119
120            // For screen readers and <TAB> navigation
121            if (yjaMenuButtonLast !== null) {
122                // If user press TAB, move focus to first element on navigation.
123                yjaMenuButtonLast.addEventListener("keydown", (evt) => {
124                    const focusable = focusables(yjaMenuWrapper);
125                    if (evt.key === "Tab") {
126                        evt.preventDefault()
127                        yjaMenuButtonClose.focus();
128                    }
129                }, false);
130
131                // Close action
132                yjaMenuButtonLast.addEventListener('click', () => {
133                    modifyDomElements(config,true);
134                    closeMobileMenu(yjaMenuWrapper, yjaMenuToggler);
135                }, false);
136            }
137
138
139            // Close by clicking outside navigation panel
140            yjaMenuWrapper.addEventListener('click', (evt) => {
141                const panel = yjaMenuWrapper.querySelector('.yja-mobile-navigation__content');
142                const inside = panel.contains(evt.target);
143                if (!inside) {
144                    modifyDomElements(config,true);
145                    closeMobileMenu(yjaMenuWrapper, yjaMenuToggler);
146                }
147            }, false);
148
149            // Close by ESC key
150            yjaMenuWrapper.addEventListener('keydown', (evt) => {
151                if (evt.key === "Escape") {
152                    modifyDomElements(config,true);
153                    if (Array.isArray(yjaMenuToggler)) {
154                        yjaMenuToggler.forEach(toggler => {
155                            closeMobileMenu(yjaMenuWrapper, toggler);
156                        })
157                    } else {
158                        closeMobileMenu(yjaMenuWrapper, yjaMenuToggler);
159                    }
160                }
161            }, false);
162
163        }
164    },false)
165}
166
167function openMobileNavigation(toggler, menuWrapper,config) {
168    const body = document.querySelector('body');
169    // Hide other DOM elements from screen readers
170    modifyDomElements(config);
171    toggler.setAttribute('aria-expanded','true');
172    menuWrapper.classList.add('yja-mobile-navigation--open');
173    menuWrapper.classList.remove('yja-mobile-navigation--slide-out');
174    body.classList.add('mobile-navigation-visible')
175
176    // Add timeout to enable animations
177    setTimeout(() => {
178        menuWrapper.classList.add('yja-mobile-navigation--slide-in');
179    }, 100);
180
181    // Move focus on navigation element
182    setTimeout(() => {
183        const FOCUSABLE_ELEMENTS = ['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA'];
184        let focusablesElements = focusables(menuWrapper);
185
186        // Check if toggler has data-focus-target set
187        // if has focus on element or its first focusable child, that was defined in data-focus-target attribute
188        if (toggler?.dataset?.focusTarget) {
189            const focusTarget = document.getElementById(toggler?.dataset?.focusTarget);
190            if (!focusTarget) return;
191            if (focusTarget.hasAttribute('tabIndex') && focusTarget.getAttribute('tabIndex') === '-1') return;
192
193            if (FOCUSABLE_ELEMENTS?.includes(focusTarget?.nodeName)) {
194                focusablesElements = focusTarget;
195            } else {
196                focusablesElements = getActiveLink(focusTarget);
197            }
198
199            // Set focus on correct element
200            if (focusablesElements instanceof Array)  focusablesElements[0].focus();
201            else focusablesElements.focus();
202
203        } else {
204            focusablesElements[0].focus();
205        }
206
207    },500);
208}
209
210function getActiveLink(focusTarget) {
211    let item;
212    if (focusTarget.classList.contains('nav')) {
213        let currentPage = focusTarget.querySelector('[aria-current="page"]');
214
215        if (!currentPage) item = focusables(focusTarget)
216        else {
217            if (currentPage.nodeName === 'LI') item = focusables(currentPage);
218            if (currentPage.nodeName === 'A')  item = currentPage;
219        }
220    } else {
221        item = focusables(focusTarget)
222    }
223
224    return item;
225}
226
227function focusables(wrapper) {
228    const focusable = [...wrapper.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')];
229    return focusable;
230}
231
232const closeMobileMenu = (yjaMenuWrapper,yjaMenuMainToggler) => {
233    const body = document.querySelector('body');
234
235    yjaMenuWrapper.classList.remove('yja-mobile-navigation--slide-in');
236    yjaMenuWrapper.classList.add('yja-mobile-navigation--slide-out');
237
238    if (yjaMenuMainToggler.length > 1) yjaMenuMainToggler.forEach(toggle => toggle.setAttribute('aria-expanded', 'false'));
239    else yjaMenuMainToggler.setAttribute('aria-expanded', 'false');
240
241    body.classList.remove('mobile-navigation-visible');
242
243    // Add timeout to enable animations
244    setTimeout(() => {
245        yjaMenuWrapper.classList.remove('yja-mobile-navigation--open');
246
247        if (yjaMenuMainToggler.length > 1) yjaMenuMainToggler.forEach(toggle => toggle.setAttribute('aria-expanded', 'false'));
248        else yjaMenuMainToggler.setAttribute('aria-expanded', 'false');
249
250        // Move focus out of navigation element
251        if (yjaMenuMainToggler.length > 1) yjaMenuMainToggler[0].focus();
252        else yjaMenuMainToggler.focus()
253    }, 500);
254}
255
256function getDisableElements(elements) {
257    if (!Array.isArray(elements)) return;
258    let domElements = []
259    elements.forEach(item =>{
260        let type = item.charAt(0)
261        switch(type) {
262            case '.':
263                let items = [...document.querySelectorAll(item)]
264                if (document.querySelectorAll(item)) items.forEach(item => domElements = [...domElements, item])
265                break;
266            case '#':
267                if (document.querySelector(item)) domElements = [...domElements, document.querySelector(item)]
268                break;
269            default:
270                return
271        }
272    })
273    return domElements;
274}
275
276function modifyDomElements(config,state) {
277    if (Array.isArray(config.elementsToHide) && config.elementsToHide.length > 0) {
278        config.elementsToHide.forEach(item => config.excludedElements =  [...config.excludedElements, item])
279    }
280    if (typeof config.elementsToHide === 'string' && config.elementsToHide !== '') config.excludedElements =  [...config.excludedElements, config.elementsToHide]
281
282    const elements = getDisableElements(config.excludedElements);
283    setVisibilityOfDomElements(elements,state,config)
284}
285
286function setVisibilityOfDomElements(items, show,config) {
287    let hide = show ? 'false' : 'true';
288    if (items.length > 0) {
289        let focusable = []
290        items.forEach(elem => {
291            let arr = elem.querySelectorAll(config.focusableElements)
292            arr.forEach(a => focusable = [...focusable, a]);
293            elem.setAttribute('aria-hidden',hide);
294
295            if (hide === 'true') elem.setAttribute('tabIndex', '-1');
296            else elem.removeAttribute('tabIndex');
297        })
298
299        if (focusable.length > 0) {
300            if (hide === 'true') focusable.forEach(e => e.setAttribute('tabIndex', '-1'));
301            else focusable.forEach(e => e.removeAttribute('tabIndex'));
302        }
303    }
304}

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.