PageSourceSearch

https://www.portmarnock.com/wp-content/uploads/fields-options/asse…/js/fields-options-js.min.js?ver=1773661744

js portmarnock.com collected 2026-09-26 02:22:07 UTC 46,291 bytes, 1,364 lines download raw bytes

1
2class Accessibility {
3    constructor() {        
4        this.accessibility = document.querySelector('.js-accessibility');
5        if(!this.accessibility) return;
6        this.toolbar_icon = this.accessibility.querySelector('.js-accessibility-icon');
7        this.close_toolbar = this.accessibility.querySelector('.js-close-toolbar');
8        var open_shortcut = this.accessibility.querySelector('.js-shortcut-open');
9        var close_shortcut = this.accessibility.querySelector('.js-shortcut-close');
10        this.items = this.accessibility.querySelectorAll('.js-accessibility-items li a');
11        this.has_tab = false;
12        this.accessibility_option = JSON.parse(this.accessibility.dataset.option)  
13        this.variables = {
14            'increase' : 0,
15            'grayscale': false,
16            'high_contrast': false,
17            'negative_contrast': false,
18            'light_background': false,
19            'links_underline': false,
20            'readable_font': false,
21            'text_spacing': 0,
22            'line_height': 0,
23            'text_align': 0,
24            'play_animations': false,
25        }
26
27        this.init();
28        this.openToolbar();
29        this.closeToolbar();
30        this.toggle_on_click(open_shortcut, this.accessibility, 'shortcut-open')
31        this.toggle_on_click(close_shortcut, this.accessibility, 'shortcut-open');
32        // this.check_item_status();
33        this.items_click();
34    }
35
36    init() {
37        this.get_general()
38        this.manage_html_tag();
39        this.manage_image();
40        this.manage_ul();
41        this.manage_li();
42        this.manage_anchor();
43        this.manage_button();
44        this.manage_heading();
45        this.manage_input();
46        this.manage_select();
47        this.manage_form();
48        this.manage_splide();
49        this.manage_iframe();
50        this.manage_label();
51        this.manage_existing_skip_content('main-content');
52        new Menu(this.accessibility_option);
53        
54        this.manage_screen_reader();
55        if(this.general.skip_content) {
56            this.skip_to_content();
57        }
58    }
59
60    openToolbar() {
61        var _self = this;
62        this.popup = this.accessibility.querySelector('.js-popup-inner'); 
63        _self.toolbar_icon.addEventListener('click', () => {
64            this.accessibility.classList.add('is-open')
65            this.popup.style.height = '500px'
66            _self.popupIsInViewpost();
67        });
68    }
69
70    closeToolbar() {
71        var _self = this;
72
73        this.close_toolbar.addEventListener('click', () => {
74            this.accessibility.classList.remove('is-open')
75            this.popup.style.height = '0px'
76            this.popup.style.bottom = 0
77        })
78    }
79
80    // Ensure the <html> tag has a lang attribute for accessibility
81    manage_html_tag() {
82        var html_tag = document.querySelector('html');
83        this.has_attribute(html_tag, 'lang', 'en-US');
84    }
85
86    get_general() {    
87        this.general = this.accessibility_option.general;
88        
89        if(this.general.outline_focus) {
90            document.body.classList.add('fo-outline-focus');
91        }
92    }
93
94    // Ensure all <img> tags have an alt attribute for accessibility
95    manage_image() {
96        var images = document.querySelectorAll('img');
97        
98        images.forEach((image) => {
99            
100            if(image.getAttribute('alt') === '' || !image.hasAttribute('alt')) {
101                image.setAttribute('alt', 'Image');
102            }
103        });
104    }
105
106    manage_screen_reader() {
107        var _self = this
108        document.addEventListener('click', function(event) {
109            const element = event.target;
110            const text = _self.getDirectInnerText(element);            
111            const has_screen_reader = document.body.classList.contains('fo-accessibility__has-screen_reader');
112            
113            
114            if(text !== '' && has_screen_reader) {
115                _self.speakText(text, element);
116            }
117            
118        })
119    }
120
121    speakText(text, elem) {
122        if (typeof speechSynthesis === "undefined") {
123            return;
124        }
125        
126        const speak_elem = document.querySelector('.fo-speak-text') ?? false
127        if(speak_elem) {
128            speak_elem.classList.remove('fo-speak-text');
129            window.speechSynthesis.cancel();
130        }
131
132        elem.classList.add('fo-speak-text');
133        let speech = new SpeechSynthesisUtterance(text);
134        
135        speech.lang = "en-US";
136        speech.rate = 1;
137        speech.pitch = 1;
138        window.speechSynthesis.speak(speech);
139        speech.onend = function() {
140            elem.classList.remove('fo-speak-text');
141        };
142    }
143
144    getDirectInnerText(element) {
145        return Array.from(element.childNodes)
146            .filter(node =>
146 node.nodeType === Node.TEXT_NODE) // Select only text nodes
147            .map(node => node.nodeValue.trim()) // Get text content and trim spaces
148            .join(""); // Join text if multiple text nodes exist
149    }
150    
151    /**
152     * Manages accessibility attributes and behavior for all anchor tags on the page
153     * - Adds appropriate aria-labels for social media links
154     * - Adds tabindex for keyboard navigation
155     * - Adds placeholder text for empty links
156     * - Sets default href if missing
157     * - Adds aria-labels from title or innerText
158     * - Handles role attributes for navigation menus and button-like links
159     * - Adds visual and semantic indicators for links that open in new tabs
160     */
161    manage_anchor() {
162        var anchor = document.querySelectorAll('a');
163        
164        anchor.forEach((a) => {            
165            // Check if this is a social media link
166            var socialMediaName = this.getSocialMediaName(a.href);
167            if (socialMediaName) {
168                // Add appropriate aria-label for social media links
169                a.setAttribute('aria-label', socialMediaName);
170            } else {
171                // Add keyboard navigation support
172                this.has_attribute(a, 'tabindex', '0');
173                // Add placeholder text for empty links
174                this.inner_text(a, 'Link');
175                // Set default href if missing
176                this.has_attribute(a, 'href', 'javascript:void(0);');
177                const title = a.getAttribute('title') ?? false;
178                // Use title attribute for aria-label if available
179                if(title) {
180                    this.has_attribute(a, 'aria-label', title);
181                } else {             
182                    // Otherwise use link text for aria-label
183                    this.has_attribute(a, 'aria-label', a.innerText);
184                }
185                
186                if(this.general.role_link) {
187                    // Add appropriate role attributes
188                    if(this.isNavigationMenu(a)) {
189                        // this.has_attribute(a, 'role', 'menuitem');
190                    } else if(a.getAttribute('href') === '#') {
191                        this.has_attribute(a, 'role', 'button');
192                    } else {
193                        
194                    }
195                }
196
197                // Handle links that open in new tabs
198                if(a.getAttribute('target') === '_blank') {
199                    // Add security attributes
200                    this.has_attribute(a, 'rel', 'noopener noreferrer');
201
202                    var element_style = window.getComputedStyle(a);
203                    // Only add visual indicator for static/relative positioned links
204                    if(element_style.position === 'static' || element_style.position === 'relative') {
205                        a.classList.add('fo-target-blank-link')
206                    
207                        // Add screen reader text
208                        const new_span = document.createElement('span');
209                        new_span.classList.add('fo-target-blank-text');
210                        new_span.innerText = 'Opens in new tab';
211                        a.appendChild(new_span);
212                    }
213                }
214            }  
215        });
216    }
217
218    /**
219     * Gets social media name from a URL
220     * @param {string} url - The URL to check
221     * @returns {string|false} - The social media name or false if not a social media URL
222     */
223    getSocialMediaName(url) {
224        if (!url) return false;
225        
226        const socialMedia = [
227            { domain: 'facebook.com', name: 'Facebook' },
228            { domain: 'fb.com', name: 'Facebook' },
229            { domain: 'twitter.com', name: 'Twitter' },
230            { domain: 'x.com', name: 'Twitter' },
231            { domain: 'instagram.com', name: 'Instagram' },
232            { domain: 'linkedin.com', name: 'LinkedIn' },
233            { domain: 'youtube.com', name: 'YouTube' },
234            { domain: 'youtu.be', name: 'YouTube' },
235            { domain: 'pinterest.com', name: 'Pinterest' },
236            { domain: 'tiktok.com', name: 'TikTok' },
237            { domain: 'snapchat.com', name: 'Snapchat' },
238            { domain: 'reddit.com', name: 'Reddit' },
239            { domain: 'tumblr.com', name: 'Tumblr' },
240            { domain: 'whatsapp.com', name: 'WhatsApp' }
241        ];
242        
243        for (const platform of socialMedia) {
244            if (url.includes(platform.domain)) {
245                return platform.name;
246            }
247        }
248        
249        return false;
250    }
251
252    /**
253     * Sets an attribute on an HTML element if it doesn't already exist
254     * @param {HTMLElement} tag - The HTML element to modify
255     * @param {string} attr - The attribute name to set
256     * @param {string} text - The attribute value to set
257     * Special handling for aria-label:
258     * - If aria-label exists but is empty, tries to generate label from element's classes
259     * - If aria-label doesn't exist, sets it to the provided text
260     * For other attributes:
261     * - Simply sets the attribute if it doesn't exist
262     */
263    has_attribute(tag, attr, text) {
264        if (attr === 'aria-label') {
265            if(tag.getAttribute(attr) === '' || tag.getAttribute(attr) === null) {
266                var aria_text = this.class_to_srting(tag); 
267                if(text) {
268                    aria_text = text;
269                }
270                tag.setAttribute(attr, aria_text);  
271                              
272            }  else if(!tag.hasAttribute(attr)) {
273                tag.setAttribute(attr, text);
274            }           
275        } else if(!tag.hasAttribute(attr)) {
276            tag.setAttribute(attr, text);
277        }
278    }
279
280    /**
281     * Adds inner text to an HTML element if it is empty
282     */
283
284    inner_text(tag, text='text') {
285        if(tag.innerText.trim() === '') {
286            const new_span = document.createElement('span');
287            new_span.classList.add('fo-none-text');
288            new_span.innerText = text;
289            tag.appendChild(new_span);
290        }
291    }
292
293    /**
294     * Ensures all heading elements (h1-h6) are accessible by:
295     * - Adding inner text if missing.
296     * - Setting aria-label based on inner text or class name.
297     */
298    manage_heading() {
299        var headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
300        
301        headings.forEach((heading) => {
302            this.inner_text(heading, 'Heading');
303            var aria_text = heading.innerText;
304            if(aria_text === '') {
305                aria_text = this.class_to_srting(heading);
306            } 
307
308            if(aria_text === '') {
309                aria_text = 'Heading';
310            }
311            this.has_attribute(heading, 'aria-label', aria_text);
312        });
313    }
314
315    /**
316     * Ensures all <input> elements are accessible by:
317     * - Adding an aria-label based on the input's name or placeholder.
318     * - Adding tabindex="0" for keyboard navigation.
319     */
320    manage_input() {
321        var inputs = document.querySelectorAll('input');
322        
323        inputs.forEach((input) => {
324            const name = input.getAttribute('name') ?? false;
325            if(input.type === 'submit') {
326                this.has_attribute(input, 'aria-label', input.value);
327                return;
328            }
329            if(name) {
330                this.has_attribute(input, 'aria-label', name);
331            } else {
332                this.has_attribute(input, 'aria-label', input.getAttribute('placeholder'));
333            }
334            this.has_attribute(input, 'tabindex', '0');
335        });
336    }
337
338    /**
339     * Ensures all <select> elements are accessible by:
340     * - Adding an aria-label based on the select's name or a default value.
341     * - Adding tabindex="0" for keyboard navigation.
342     */
343    manage_select() {
344        var select = document.querySelectorAll('select');
345
346        select.forEach((input) => {
347            const name = input.getAttribute('name') ?? false;
348            if(name) {
349                this.has_attribute(input, 'aria-label', name);
350            } else {
351                this.has_attribute(input, 'aria-label', 'Select Option');
352            }
353            this.has_attribute(input, 'tabindex', '0');
354        });
355        
356    }
357
358    /**
359     * Ensures all <form> elements are accessible by:
360     * - Adding an aria-label attribute with a unique label for each form.
361     */
362    manage_form() {
363        var form_tag = document.querySelectorAll('form');
364        if(form_tag.length === 0) return;
365
366        form_tag.forEach((form, i) => {
367            this.has_attribute(form, 'aria-label', 'Form ' + i);
368        });
369    }
370
371    /**
372     * Ensures all elements with the .splide class (typically sliders/carousels) are accessible by:
373     * - Adding an aria-label attribute to each slider for screen readers.
374     */
375    manage_splide() {
376        var splide = document.querySelectorAll('.splide');
377        if(splide.length === 0) return;
378        
379        splide.forEach((slider, i) => {
380            slider.setAttribute('aria-label', 'Slider'+ i);
381        });
382    }
383
384    /**
385     * Ensures all <iframe> elements are accessible by:
386     * - Adding a unique title attribute to each iframe for screen readers.
387     */
388    manage_iframe() {
389        var iframe = document.querySelectorAll('iframe');
390        if(iframe.length === 0) return;
391
392        iframe.forEach((frame, i) => {
393            this.has_attribute(frame, 'title', 'Iframe'+ i);
394        });
395    }
396
397    manage_label()  {
398        var labels = document.querySelectorAll('label');
399        if(labels.length === 0) return;
400        labels.forEach((label) => {
401            // Ensure label has a for attribute that matches an input's id
402            const inputId = label.getAttribute('for');
403            if(inputId) {
404                const input = document.getElementById(inputId);
405                if(input) {
406                    this.has_attribute(label, 'aria-label', input.getAttribute('name') || input.getAttribute('placeholder') || 'Label');
407                } else {
408                    this.has_attribute(label, 'aria-label', 'Label for ' + inputId);
409                }
410            } else {
411                this.has_attribute(label, 'aria-label', 'Label');
412            }
413        });
414    }
415
416
417    /**
418     * Modifies an existing skip-to-content link by replacing its href with 'javascript:void(0)'.
419     * This prevents the default jump behavior and allows for custom skip logic.
420     * @param {string} id - The id of the element to skip to.
421     */
422    manage_existing_skip_content(id) {
423        var skip = document.querySelector('[href="#'+ id +'"]');
424        if(!skip) return;
425        skip.href = 'javascript:void(0)';        
426    }
427
428    /**
429     * Ensures all <button> elements are accessible by adding tabindex, inner text, and ARIA roles.
430     * - Adds tabindex="0" for keyboard navigation.
431     * - Adds inner text if missing.
432     * - Adds role="menuitem" if the button is part of a navigation menu.
433     */
434    manage_button() {
435        var buttons = document.querySelectorAll('button');
436        
437        buttons.forEach((btn) => {
438            this.has_attribute(btn, 'tabindex', '0');
439            this.inner_text(btn, 'button');    
440            if(this.isNavigationMenu(btn)) {
441                // this.has_attribute(btn, 'role', 'menuitem');
442            }   
443        });
444    }
445
446    // Adds appropriate ARIA roles to <ul> elements for accessibility
447    manage_ul() {
448        var ul = document.querySelectorAll('ul');
449
450        ul.forEach((list) => {
451            if(this.isNavigationMenu(list)) {
452                // If the <ul> is part of a navigation menu, set role to 'menu'
453                // this.has_attribute(list, 'role', 'menu');
454            } else {
455                // Otherwise, you could set role to 'list' if needed
456                // this.has_attribute(list, 'role', 'list');
457            }
458            // Optionally, generate a role text from class names
459            // var role_text = this.class_to_srting(list.classList);
460            // if(!role_text) {
461            //     role_text = 'Item-list';
462            // } 
463            // this.has_attribute(list, 'role', role_text)
464        });
465    }
466
467    /**
468     * Determines if a <ul> element is part of a navigation menu for accessibility purposes.
469     * Adds an aria-label to the closest <nav> if found.
470     * @param {HTMLElement} ul - The <ul> element to check.
471     * @returns {boolean} True if the <ul> is considered a navigation menu, otherwise false.
472     */
473    isNavigationMenu(ul) {
474        // Check if <ul> is inside a <nav> element
475        if (ul.closest('nav')) {
476            var aria_text = this.class_to_srting(ul.closest('nav'));
477            if(!aria_text) {
478                aria_text = 'Nav';
479            }
480            this.has_attribute(ul.closest('nav'), 'aria-label', aria_text);
481            return true;
482        }
483        
484        // Check if <ul> has navigation-related classes
485        const navClasses = ['nav', 'navbar', 'menu', 'navigation', 'dropdown-menu'];
486        if (navClasses.some(cls => ul.classList.contains(cls))) return true;
487    
488        // Check if <ul> has an ARIA role related to navigation
489        // const role = ul.getAttribute('role');
490        // if (role === 'navigation' || role === 'menu') return true;
491    
492        // Check if <li> elements contain <a> tags (common in nav menus)
493        // if (ul.querySelectorAll('li > a').length > 0) return true;
494    
495        // If none of the conditions match, it's not a nav menu
496        return false;
497    }
498
499    // Adds appropriate ARIA roles to <li> elements for accessibility
500    manage_li() {
501        var li = document.querySelectorAll('li');
502
503        li.forEach((list) => {    
504            // If the <li> is part of a navigation menu, set role to 'none'
505            if(this.isNavigationMenu(list)) {
506                // list.setAttribute('role', 'none');
507            }  
508        });
509    }
510
511    class_to_srting(tag, get_srting = false) {
512        const class_list_array = tag.classList;
513        if(class_list_array.length === 0) return false;
514        for (let str of class_list_array) {
515            var srting = this.cleanAndFormatString(str);
516            var meaningful = this.isMeaningfulString(srting);
517
518            if(meaningful) {
519                return srting;
520            }            
521        };
522        return false;
523    }
524
525    cleanAndFormatString(str) {
526        return str
527            .replace(/[_-]+/g, ' ') // Replace multiple hyphens or underscores with a single space
528            .trim() // Remove leading/trailing spaces
529            .replace(/\b\w/g, char => char.toUpperCase()); // Capitalize first letter of each word
530    }
531
532    isMeaningfulString(str) {
533        return /\b[a-zA-Z]{3,}\b/.test(str); // Checks if words contain letters and are at least 3 characters long
534    }
535
536    skip_to_content() {
537        this.create_skip_to_content_html();
538        this.skip_link_click();
539    }
540
541    create_skip_to_content_html() {
542        // Create the container div with landmark role
543        const skipContainer = document.createElement('div');
544        skipContainer.setAttribute('role', 'region');
545        skipContainer.setAttribute('aria-label', 'Skip link');
546
547        // Create the skip link
548        const new_skip_link = document.createElement('a');
549        new_skip_link.classList.add('fo-skip-link');
550        new_skip_link.setAttribute('href', 'javascript:void(0)');
551        new_skip_link.setAttribute('tabindex', '0');
552        new_skip_link.innerText = 'Skip to content';
553
554        // Add the skip link to the container
555        skipContainer.appendChild(new_skip_link);
556
557        // Insert at the beginning of the body
558        document.body.insertAdjacentElement("afterbegin", skipContainer);
559    }
560
561    skip_link_click() {
562        const link = document.querySelector('.fo-skip-link');
563        link.addEventListener('click', (e) => {
564            e.preventDefault();
565            function findFirstElementWithTag(tag) {
566                return document.querySelector(tag);
567            }
568    
569            const h1Element = findFirstElementWithTag('h1');
570            const h2Element = findFirstElementWithTag('h2');
571            const introTextElement = findFirstElementWithTag('.voyager-intro-text');
572    
573            // Determine the first element to scroll to
574            let targetElement = null;
575    
576            // Prioritize h1 > h2 > .voyager-intro-text
577            if (h1Element) {
578                targetElement = h1Element;
579            } else if (h2Element) {
580                targetElement = h2Element;
581            } else if (introTextElement) {
582                targetElement = introTextElement;
583            }
584    
585            // Scroll to the target element if found
586            if (targetElement) {
587                targetElement.scrollIntoView({ behavior: 'smooth' });
588                targetElement.setAttribute('tabindex', '-1'); // Make the target element focusable
589                targetElement.focus(); // Focus the target element for screen readers
590            }
591        });
592    }
593
594    toggle_on_click(elem, selector, toggle_class = 'is-open') {
595        var _self = this;
596        if(elem) {
597            elem.addEventListener('click', (e) => {
598                e.preventDefault();
599                selector.classList.toggle(toggle_class);
600            });
601        }
602    }
603
604    popupIsInViewpost() {       
605        var toolbar_position = this.accessibility_option.toolbar_position;
606        const rect = this.popup.getBoundingClientRect();
607        if(toolbar_position === 'bottom_right' || toolbar_position === 'bottom_left') {
608            const white_space = window.innerHeight - ((window.innerHeight - rect.bottom) + 500)
609            
610            if(white_space < 30) {
611                this.popup.style.bottom = `${white_space - 10}px`;                
612            }
613        }
614
615        
616        if(toolbar_position === 'top_right' || toolbar_position === 'top_left') {
617            const white_space = window.innerHeight - (rect.top + 500)
618
619            if(white_space < 0) {
620                this.popup.style.top = `${white_space - 10}px`;
621            }
622        }        
623    }
624
625    check_item_status() {
626        for (let key in this.variables) {
627            let item_class = 'js-item-'+ key;
628            let body_class = 'fo-accessibility__has-'+ key;
629            let class_add = 'body';
630            if(key === 'increase') {
631                item_class = 'js-item-increase';
632                body_class = 'fo-accessibility__resize-font-0';
633            }
634
635            if(key === 'grayscale' || key === 'negative_contrast') {
636                class_add = 'html';
637            }
638
639            this.manage__active_class(this.variables[key], item_class, body_class, class_add);
640        }
641    }
642
643    manage__active_class(condition, item_class, body_class, class_add = 'body') {
644        if(class_add === 'body') {
645            class_add = document.body;
646        } else {
647            class_add = document.documentElement;
648        }
649
650        if(condition) {
651            this.accessibility.querySelector(`.${item_class}`).classList.add('is-active');
652            class_add.classList.add(body_class);
653        } else {
654            this.accessibility.querySelector(`.${item_class}`).classList.remove('is-active');
655            class_add.classList.remove(body_class);
656        }
657    }
658    
659    items_click() {
660        this.items.forEach((item) => {
661            
662            item.addEventListener('click', (e) => {
663                e.preventDefault();
664                var action = item.getAttribute('data-action');
665                
666                switch (action) {
667                    case "increase":
668                        this.handle_increase();
669                        break;
670                    case "decrease":
671                        this.handle_decrease();
672                        break;
673                    case "text_spacing":
674                        this.handle_four_click('text_spacing');
675                        break;
676                    case "line_height":
677                        this.handle_four_click('line_height');
678                        break;
679                    case "text_align":
680                        this.handle_four_click('text_align');
681                        break;
682                    case "reset":
683                        this.handle_reset();
684                        break;
685                    default:
686                        this.variables[action] = !this.variables[action];
687                }
688                this.check_item_status();
689            });
690        });
691    }
692
693    handle_increase() {
694        if(this.variables.increase !== 8) {
695            document.body.classList.remove('fo-accessibility__resize-font-' + this.variables.increase);
696            this.variables.increase += 1;
697            document.body.classList.add('fo-accessibility__resize-font-' + this.variables.increase);
698        }
699    }
700
701    handle_decrease() {
702        if(this.variables.increase !== 0) {
703            document.body.classList.remove('fo-accessibility__resize-font-' + this.variables.increase);
704            this.variables.increase -= 1;
705            document.body.classList.add('fo-accessibility__resize-font-' + this.variables.increase);
706        }
707    }
708
709    handle_four_click(value) {
710        if(this.variables[value] !== 4) {
711            document.body.classList.remove('fo-accessibility__'+ value +'-' + this.variables[value]);
712            this.variables[value] += 1;
713            document.body.classList.add('fo-accessibility__'+ value +'-' + this.variables[value]);
714        } else {
715            document.body.classList.remove('fo-accessibility__'+ value +'-' + this.variables[value]);
716            this.variables[value] = 0;
717        }
718    }
719
720    handle_reset() {       
721        document.body.classList.remove('fo-accessibility__resize-font-' + this.variables.increase);
722        document.body.classList.remove('fo-accessibility__text_spacing-' + this.variables.text_spacing);
723        document.body.classList.remove('fo-accessibility__line_height-' + this.variables.line_height);
724        document.body.classList.remove('fo-accessibility__text_align-' + this.variables.text_align);
725         
726        this.variables = {
727            'increase' : 0,
728            'grayscale': false,
729            'high_contrast': false,
730            'negative_contrast': false,
731            'light_background': false,
732            'links_underline': false,
733            'readable_font': false,
734            'text_spacing': 0,
735            'line_height': 0,
736            'text_align': false,
737            'play_animations': false,
738        }
739    }
740}
741
742document.addEventListener('DOMContentLoaded', function() {
743    setTimeout(function() {
744        new Accessibility();
745    }, 1000); // 1000 milliseconds = 1 secon
746});
747class Menu {
748    constructor(accessibility_option) {
749        this.accessibility_option = accessibility_option;
750		this.keys = {
751			ARROW_DOWN: 'ArrowDown',
752			ARROW_UP: 'ArrowUp',
753			ARROW_RIGHT: 'ArrowRight',
754			ARROW_LEFT: 'ArrowLeft',
755			SPACE: ' ',
756			ESCAPE: 'Escape',
757			HOME: 'Home',
758			END: 'End',
759			ENTER: 'Enter'
760		};
761        // this.hamburger_close_btn = '';
762        this.init();
763        this.clickOutside();
764        // this.menuOpener();
765        // this.closeSubmenuByHover();		
766    }   
767
768    init() {
769        this.menu_element = this.accessibility_option?.menu_element_selector?.menu_wrapper_selector;      
770        this.is_hamburger = this.accessibility_option?.menu_element_selector?.hamburger ?? false;  
771		this.center_logo = this.accessibility_option?.menu_element_selector?.center_logo ?? false;
772		var menu_closer = this.accessibility_option?.menu_element_selector?.menu_closer ?? false;
773		var menu_opener = this.accessibility_option?.menu_element_selector?.menu_opener ?? false;
774		
775		if(menu_opener) {
776			this.menu_opener = document.querySelector(menu_opener);
777		}
778
779		if(menu_closer) {
780			this.menu_closer = document.querySelector(menu_closer);
781		}
782        
783        if(!this.menu_element) return false;
784        this.menu_selector = document.querySelector(this.menu_element);
785        this.setupMenuRoles();
786		this.bindEvent();	
787    }
788
789    setupMenuRoles() {
790        this.menu = document.querySelector(this.menu_element);		
791        if(!this.menu) return;
792
793		let uls = ':scope > ul';
794		if(this.center_logo) {
795			uls = ':scope > div > ul';
796		}
797
798        if(this.menu.tagName !== 'UL') {
799            this.menu = this.menu.querySelectorAll(uls);
800            this.menu.forEach(item => {
801                this.menuRole(item);
802            });
803        } else {
804            this.menuRole(this.menu);
805        }
806
807        
808    }
809
810    menuRole(items) {
811        items.setAttribute('role', 'menubar');
812        items.setAttribute('aria-label', 'Main Navigation Menu');
813
814        const menuItems = items.querySelectorAll('li');
815        menuItems.forEach(item => {
816            item.setAttribute('role', 'none');
817            const anchor = item.querySelector('a');
818            if(!anchor) return;
819            anchor.setAttribute('role', 'menuitem');
820            if(item.querySelector('ul')) {
821				const hasButton = this.hasButtonInItem(item);		
822				if(!hasButton) {
823					this.createSubmenuButton(item, anchor);
824				}		
825                item.querySelector('ul').setAttribute('role', 'menu');
826                anchor.setAttribute('aria-haspopup', 'true'); // Indicate that it has a submenu
827                anchor.setAttribute('aria-expanded', 'false'); // Initially not expanded
828            }
829        });
830
831		if(this.is_hamburger && this.menu_opener) {
832			this.menu_opener.setAttribute('aria-haspopup', 'dialog');
833			this.menu_opener.setAttribute('aria-expanded', 'false');
834			this.menu_opener.setAttribute('aria-label', 'Open menu');
835			
836			if(this.menu_closer && this.menu_opener !== this.menu_closer) {
837				this.menu_closer.setAttribute('aria-label', 'Close menu');
838			}
839
840			document.querySelector(this.menu_element).setAttribute('role', 'dialog');
841			this.hamburgerClick();
842		}
843    }
844
845	hasButtonInItem(item) {
846		// Get only direct children of the li element, excluding ul elements
847		const directChildren = Array.from(item.children).filter(child => child.tagName !== 'UL');
848		// Return the button element if found, otherwise null
849		return directChildren.find(child => child.tagName === 'BUTTON') || null;
850	}
851
852	createSubmenuButton(item, anchor) {
853		const button = document.createElement('button');
854		button.setAttribute('aria-expanded', 'false');
855		button.setAttribute('aria-label', `Expand ${anchor.innerText} submenu`);
856		button.classList.add('fo-submenu-btn');
857		button.textContent = '▼';
858		item.appendChild(button);		
859	}
860
861	bindEvent() {
862		document.addEventListener('keydown', this.handleKeydown.bind(this));
863	}
864
865	handleKeydown(e) { 
866        var menu_opener = this.menu_opener;
867		if(menu_opener && this.is_hamburger) {
868			if(document.activeElement === menu_opener) {
869				const hamburger_key_actions = {
870					[this.keys.SPACE]: () => this.openHamburger(),
871					[this.keys.ARROW_DOWN]: () => this.openHamburger(),
872					[this.keys.ENTER]: () => this.openHamburger()
873				}
874
875				const hamburger_action = hamburger_key_actions[e.key];
876				if(hamburger_action) {
877					e.preventDefault();
878					hamburger_action();
879					return;
880				}
881			}
882		}
883
884		if(!this.isFocusInMenu()) return;		
885		
886		const key_actions = {
887			[this.keys.ARROW_DOWN]: () => this.handleArrowDown(),
888			[this.keys.ARROW_UP]: () => this.handleArrowUp(),
889			[this.keys.ARROW_RIGHT]: () => this.handleArrowRight(),
890			[this.keys.ARROW_LEFT]: () => this.handleArrowLeft(),
891			[this.keys.SPACE]: () => this.handleSpace(),
892			[this.keys.ESCAPE]: () => this.handleEsc(),
893			[this.keys.HOME]: () => this.jumpToPosition(0),
894			[this.keys.END]: () => this.jumpToPosition(-1)
895		}
896		
897		const action = key_actions[e.key];
898		if(action) {
899			e.preventDefault();
900			setTimeout(action, 0);				
901		}
902        
903    } 
904
905	/**
906	 * Checks if the currently focused element is within the menu
907	 * @returns {boolean} True if focus is within menu, false otherwise
908	 */
909	isFocusInMenu() {
910		const menu_element = document.querySelectorAll(this.menu_element);
911		return Array.from(menu_element).some(menuEl => {
912			return menuEl.contains(document.activeElement);
913		});
914	}      
915
916	/**
917	 * Handles right arrow key press events
918	 * - For top level items: moves to next item and opens submenu if present
919	 * - For second level items with submenu: opens submenu and focuses first item
920	 * - For items without submenu: moves focus to next top level item
921	 */
922	handleArrowRight() {
923		let position = this.getFocusedPosition();
924		
925		if (position.isTopLevel) {			
926			this.closeAllSubmenus();
927			if(!this.centerLogoMenuFocusMove('right')) {
928				this.jumpEndToEnd('last_to_first');
929			}
930			
931			// Check if new position has submenu and open it
932			const newPosition = this.getFocusedPosition();
933			if (newPosition.hasSubmenu) {
934				this.openSubmenu();                
935			}
936			return;
937		}
938		
939		// Second level with submenu: enter submenu
940		if (position.secondLevelMenu && position.hasSubmenu) {
941			this.openSubmenu();
942			this.focusSubmenuItem();
943			return;
944		}
945		
946		// // Any other level without submenu: navigate to top level
947		if (!position.hasSubmenu) {
948			this.closeAllSubmenus();
949			if(!this.centerLogoMenuFocusMove('submenu_right')) {
950				this.moveToFirstLevel();
951			}
952		}
953	}    
954
955	handleArrowLeft() {
956		let position = this.getFocusedPosition();
957		if(position.isTopLevel) {
958			this.closeAllSubmenus();
959			if(!this.centerLogoMenuFocusMove('left')) {
960				this.jumpEndToEnd();
961			}			
962		}
963
964		if(position.secondLevelMenu) {
965			this.closeAllSubmenus();
966			this.moveToFirstLevel('previous');
967		}
968
969		if(position.thirdLevelMenu) {
970			this.moveToPreviousLevel();
971		}
972
973		position = this.getFocusedPosition();
974
975		if (position.isTopLevel && position.hasSubmenu) {
976			this.openSubmenu();
977		}
978	}
979    
980	handleArrowDown() {		
981		const position = this.getFocusedPosition();
982
983		// HORIZONTAL MENU: Context-aware navigation
984		if (position.hasSubmenu && position.isTopLevel) {
985			// Top level with submenu: Enter submenu
986			this.openSubmenu();
987			this.focusSubmenuItem();
988		} else if (!position.isTopLevel) {
989			// Already in submenu: Navigate within submenu
990			this.jumpEndToEnd('last_to_first');
991		}
992		// Top level without submenu: No action (stays in place)
993	}
994	
995	/**
996	 * PRIMARY NAVIGATION: Handles arrow up key press events
997	 * 
998	 * This is the core upward navigation method that adapts behavior based on:
999	 * - Menu type (hamburger vs horizontal)
1000	 * - Current position (top level vs submenu)
1001	 * - Submenu availability
1002	 * 
1003	 * HAMBURGER MENU BEHAVIOR:
1004	 * - Always moves to previous item in vertical list
1005	 * 
1006	 * HORIZONTAL MENU BEHAVIOR:
1007	 * - Top level with submenu: Opens submenu and focuses last item
1008	 * - Submenu levels: Moves to previous item within current submenu
1009	 */
1010	handleArrowUp() {
1011		const position = this.getFocusedPosition();
1012		// HORIZONTAL MENU: Context-aware navigation
1013		if (position.hasSubmenu && position.isTopLevel) {
1014			// Top level with submenu: Enter submenu at LAST item
1015			this.openSubmenu();
1016			this.focusSubmenuItem('last');
1017		} else if (!position.isTopLevel) {
1018			// Already in submenu: Navigate within submenu (reverse)
1019			this.jumpEndToEnd();
1020		}
1021		// Top level without submenu: No action (stays in place)
1022	}
1023
1024	handleSpace() {
1025		const position = this.getFocusedPosition();
1026		
1027		if (position.hasSubmenu) {
1028			if (!this.is_hamburger) this.openSubmenu();
1029			else this.openSubmenuByClick();
1030			this.focusSubmenuItem();
1031		} else {
1032			if (!this.is_hamburger) this.closeAllSubmenus();
1033			document.activeElement.click();
1034		}
1035	}
1036	
1037	// Handles escape key press events
1038	// Checks if escape key was pressed and if current focus is not on top level menu
1039	// If so, moves focus to previous menu level
1040	handleEsc() {
1041		const focue_possion = this.getFocusedPosition();	
1042		if(this.is_hamburger) {
1043			if(focue_possion.isTopLevel) {
1044				this.closeHamburger();
1045			} else {
1046				this.closeSubmenuByClick();
1047			}
1048		} else {
1049			if(!focue_possion.isTopLevel) {
1050				this.moveToPreviousLevel();
1051			}
1052		}
1053	}
1054
1055    getFocusedPosition() {
1056        const li = document.activeElement.closest('li');
1057        const hasSubmenu = li?.querySelector('ul');		
1058        
1059        return {
1060            isTopLevel: this.isTopLevelMenuItem(),
1061            secondLevelMenu: this.isSecondLevelMenuItem(),
1062            thirdLevelMenu: this.isThirdLevelMenuItem(),
1063			currentIndex : this.getCurrentIndex(),
1064            hasSubmenu: hasSubmenu,
1065        };
1066    }    
1067
1068	getCurrentIndex() {
1069        const li = document.activeElement.closest('li');
1070		const lis = Array.from(li.closest('ul').children).filter(el => 
1071            el.tagName === 'LI' && el.querySelector('a')
1072        );
1073        const currentIndex = lis.indexOf(li); 
1074		return {
1075			length: lis.length,
1076			index: currentIndex
1077		}
1078	}
1079
1080    isTopLevelMenuItem() {
1081        const li = document.activeElement.closest('li');
1082        if (!li) return false;
1083        
1084        // Check if this li has no parent li (meaning it's top level)
1085        return !li.parentElement.closest('li');
1086    }
1087
1088    isSecondLevelMenuItem() {
1089        const li = document.activeElement.closest('li');
1090        if (!li) return false;
1091        
1092        const parentLis = li.closest('ul').parentElement.closest('li');
1093        // Second level: has exactly one parent li
1094        return parentLis && !parentLis.parentElement.closest('li');
1095    }
1096
1097	isThirdLevelMenuItem() {
1098		const li = document.activeElement.closest('li');
1099		if (!li) return false;
1100		
1101		let parentCount = 0;
1102		let currentLi = li;
1103		
1104		// Count parent li elements
1105		while (currentLi.parentElement.closest('li')) {
1106			currentLi = currentLi.parentElement.closest('li');
1107			parentCount++;
1108		}
1109		
1110		// Third level: has exactly two parent li elements
1111		return parentCount === 2;
1112	}
1113
1114	/**
1115	 * Handles jumping between menu items at the same level
1116	 * @param {string} level - Direction to jump: 'last_to_first' or 'first_to_last'
1117	 * - Gets current focused list item
1118	 * - Gets array of all list items at same level
1119	 * - If 'last_to_first': moves to next sibling or wraps to first item
1120	 * - If 'first_to_last': moves to previous sibling or wraps to last item
1121	 * - Focuses the link in the target list item
1122	 */
1123	jumpEndToEnd(level = '') {
1124		const li = document.activeElement.closest('li');        
1125		const lis = Array.from(li.closest('ul').children).filter(el => 
1126            el.tagName === 'LI' && el.querySelector('a')
1127        );
1128        const currentIndex = lis.indexOf(li);        
1129		const next = level === 'last_to_first' 
1130			? lis[currentIndex + 1] || lis[0]
1131			: lis[currentIndex - 1] || lis[lis.length - 1];
1132		
1133		next.querySelector('a')?.focus();
1134	}
1135
1136	centerLogoMenuFocusMove(key) {
1137        const menu_element = this.menu;
1138		
1139		if(menu_element.length === 1 || !menu_element.length) return false; 
1140		
1141		const left = Array.from(menu_element[0].children);
1142		const right = Array.from(menu_element[1].children);
1143		const current = this.getTopLevelLi();
1144		
1145		const moves = {
1146			right: [
1147				[left[left.length - 1], () => menu_element[1].querySelector('a').focus()],
1148				[right[right.length - 1], () => menu_element[0].querySelector('a').focus()]
1149			],
1150			left: [
1151				[left[0], () => right[right.length - 1].querySelector('a').focus()],
1152				[right[0], () => left[left.length - 1].querySelector('a').focus()]
1153			]
1154		};
1155		
1156		const moveSet = moves[key.replace('submenu_', '')] || [];
1157		for (const [target, action] of moveSet) {
1158			if (current === target) {
1159				action();
1160				return true;
1161			}
1162		}
1163		
1164		return false;
1165	}
1166
1167	getTopLevelLi() {
1168		let li = document.activeElement.closest('li');
1169		if (!li) return null;
1170		
1171		// Keep going up until we find the top level li
1172		while (li.parentElement.closest('li')) {
1173			li = li.parentElement.closest('li');
1174		}
1175		
1176		return li;
1177	}
1178
1179	openSubmenu() {
1180		var active_element = document.activeElement;
1181        const li = active_element.closest('li');
1182		const hasButton = this.hasButtonInItem(li);
1183		if(hasButton) {
1184			hasButton.setAttribute('aria-expanded', 'true');
1185		}
1186		
1187		active_element.setAttribute('aria-expanded', 'true');
1188		let next = document.activeElement.nextElementSibling;
1189        if(!next) {
1190            next = document.activeElement.parentElement.nextElementSibling;            
1191        }
1192        
1193        if(next) {
1194            var css = window.getComputedStyle(next);
1195            next.classList.add('fo-submenu-is-expanded');
1196            
1197            if(css.display === 'none') {
1198                this.addCssRule('.fo-submenu-is-expanded', 'display: block !important;');
1199            }
1200        }
1201	}
1202
1203	closeAllSubmenus() {		
1204		const open_elements = this.menu_selector.querySelectorAll(`.fo-submenu-is-expanded`);
1205		open_elements.forEach(item => {
1206			item.classList.remove('fo-submenu-is-expanded');
1207		});
1208
1209		this.menu_selector.querySelectorAll('[aria-expanded="true"]').forEach(item => {
1210			item.setAttribute('aria-expanded', 'false');
1211		});
1212
1213	}
1214
1215
1216    /**
1217     * Adds a CSS rule to the document
1218     * @param {string} selector - CSS selector
1219     * @param {string} rules - CSS rules
1220     */
1221    addCssRule(selector, rules) {
1222        // Create a style element if it doesn't exist
1223        if (!this.styleElement) {
1224            this.styleElement = document.createElement('style');
1225            this.styleElement.type = 'text/css';
1226            document.head.appendChild(this.styleElement);
1227        }
1228        
1229        // Add the rule to the style sheet
1230        const sheet = this.styleElement.sheet;
1231        sheet.insertRule(`${selector} { ${rules} }`, sheet.cssRules.length);
1232    }
1233
1234	/**
1235	 * Focuses a submenu item based on position
1236	 * @param {string} position - Position to focus ('first' or 'last')
1237	 h* Finds the submenu of the currently focused menu item and focuses either
1238	 * the first or last link within it based on the position parameter.
1239	 * Returns early if there is no submenu.
1240	 */
1241	focusSubmenuItem(position = 'first') {
1242		const focus_position = this.getFocusedPosition();				
1243		if (!focus_position.hasSubmenu) return;
1244		
1245		const child = Array.from(focus_position.hasSubmenu.children).filter(item => 
1246            item.querySelector('a')
1247        );
1248        
1249		const target_link = position === 'last' 
1250			? child[child.length - 1].querySelector('a')
1251			: child[0].querySelector('a');
1252
1253		target_link?.focus();
1254	}
1255
1256	moveToFirstLevel(level = 'next') {
1257		let topLevelLi = this.getTopLevelLi();
1258		let focus_li = null;
1259
1260		if(level === 'next') {
1261			focus_li = topLevelLi.nextElementSibling || topLevelLi.parentElement.children[0];
1262		} else {
1263			focus_li = topLevelLi.previousElementSibling || topLevelLi.parentElement.children[topLevelLi.parentElement.children.length - 1];
1264		}
1265		this.closeAllSubmenus();
1266		focus_li.querySelector('a')?.focus();
1267		this.openSubmenu();
1268	}
1269	
1270	/**
1271	 * Jumps focus to a specific position in the current menu list
1272	 * @param {number} position - Index of the menu item to focus (0-based)
1273	 * Supports: 0 (first), -1 (last), or any valid index
1274	 */
1275	jumpToPosition(position) {
1276		const li = document.activeElement.closest('li');
1277		if (!li) return;
1278		
1279		const ul = li.closest('ul');
1280		const lis = Array.from(ul.children).filter(el => 
1281            el.tagName === 'LI' && el.querySelector('a')
1282        );
1283		
1284		// Handle negative index (e.g., -1 for last item)
1285		const targetIndex = position < 0 ? lis.length + position : position;
1286		
1287		// Ensure index is within bounds
1288		if (targetIndex >= 0 && targetIndex < lis.length) {
1289			lis[targetIndex].querySelector('a')?.focus();
1290		}
1291	}
1292
1293	// Closes any open submenus when clicking outside the menu
1294	clickOutside() {
1295		document.addEventListener('click', () => {
1296			this.closeAllSubmenus();
1297		});
1298	}
1299
1300	// Moves focus to the parent level menu item	
1301	moveToPreviousLevel() {
1302		const li = document.activeElement.closest('li'); // - Gets the current active list item
1303		const ul = li.closest('ul');
1304		const parentLi = ul.closest('li'); // - Finds its parent ul and parent li elements
1305		parentLi.classList.remove('is-open'); // - Removes the 'is-open' class from parent li
1306		parentLi.querySelector('a')?.focus(); // - Focuses the parent li's link
1307	}
1308
1309	openHamburger() {
1310		this.hamburger_button = document.activeElement;
1311		this.hamburger_button.click();
1312		this.menu_selector.querySelector('a').focus();
1313	}    
1314
1315	hamburgerClick() {
1316		var _self = this;
1317		_self.menu_opener?.addEventListener('click', () => {
1318			var expanded = _self.menu_opener.getAttribute('aria-expanded');
1319			if(expanded === 'true') {
1320				_self.menu_opener.setAttribute('aria-expanded', 'false');
1321				_self.handleMenuCloser(false);
1322			} else {
1323				_self.menu_opener.setAttribute('aria-expanded', 'true');
1324				_self.handleMenuCloser(true);
1325			}			
1326		});
1327	}
1328
1329	handleMenuCloser(status) {
1330		if(this.menu_closer && this.menu_closer !== this.menu_opener) return false;
1331
1332		if(status) {
1333			this.menu_opener.setAttribute('aria-label', 'Close menu')
1334		} else {
1335			this.menu_opener.setAttribute('aria-label', 'Open menu')
1336		}
1337	}
1338
1339	closeHamburger() {
1340		let menu_closer = this.accessibility_option?.menu_element_selector?.menu_closer ?? false;
1341		
1342		if(menu_closer) {
1343			menu_closer = document.querySelector(menu_closer);
1344		} else {
1345			menu_closer = this.hamburger_button;
1346		}
1347		menu_closer.click();
1348		this.hamburger_button.focus();
1349	}
1350
1351	openSubmenuByClick() {
1352        let button = document.activeElement.nextElementSibling;
1353		if(button.tagName !== 'BUTTON') {
1354            button = document.activeElement.closest('li'); 
1355		}		
1356        document.activeElement.setAttribute('aria-expanded', 'true');
1357        button.click();
1358	}
1359
1360	closeSubmenuByClick() {
1361		this.moveToPreviousLevel();
1362		this.openSubmenuByClick();
1363	}
1364}

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.