PageSourceSearch

https://www.thm.de/site/media/plg_system_thmaccessibility/js/skipto.js?2a7331

js thm.de collected 2026-09-24 18:11:48 UTC 69,644 bytes, 1,494 lines download raw bytes

1/*! skipto - v4.1.7 - 2023-01-13
2* https://github.com/paypal/skipto
3* Copyright (c) 2023 PayPal Accessibility Team and University of Illinois; Licensed  */
4/*@cc_on @*/
5/*@if (@_jscript_version >= 5.8) @*/
6/* ========================================================================
7* Copyright (c) <2021> PayPal and University of Illinois
8* All rights reserved.
9* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
10* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
11* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12* Neither the name of PayPal or any of its subsidiaries or affiliates nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
13* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14* ======================================================================== */
15
16(function() {
17    'use strict';
18    var SkipTo = {
19        skipToId: 'id-skip-to-js-4',
20        skipToMenuId: 'id-skip-to-menu-4',
21        domNode: null,
22        buttonNode: null,
23        menuNode: null,
24        tooltipNode: null,
25        menuitemNodes: [],
26        firstMenuitem: false,
27        lastMenuitem: false,
28        firstChars: [],
29        headingLevels: [],
30        skipToIdIndex: 1,
31        showAllLandmarksSelector: 'main, [role=main], [role=search], nav, [role=navigation], section[aria-label], section[aria-labelledby], section[title], [role=region][aria-label], [role=region][aria-labelledby], [role=region][title], form[aria-label], form[aria-labelledby], aside, [role=complementary], body > header, [role=banner], body > footer, [role=contentinfo]',
32        showAllHeadingsSelector: 'h1, h2, h3, h4, h5, h6',
33        showTooltipFocus: false,
34        showTooltipHover: false,
35        tooltipTimerDelay: 500,  // in milliseconds
36        // Default configuration values
37        config: {
38            // Feature switches
39            enableActions: false,
40            enableMofN: true,
41            enableHeadingLevelShortcuts: true,
42            enableHelp: true,
43            enableTooltip: true,
44            // Customization of button and menu
45            accesskey: '',
46            attachElement: 'header',
47            displayOption: 'static', // options: static (default), popup
48            // container element, use containerClass for custom styling
49            containerElement: 'div',
50            containerRole: '',
51            customClass: '',
52
53            // Button labels and messages
54            buttonTitle: '',   // deprecated in favor of buttonTooltip
55            buttonTitleWithAccesskey: '',  // deprecated in favor of buttonTooltipAccesskey
56            buttonTooltip: '',
57            buttonTooltipAccesskey: 'Shortcut Key: $key',
58            buttonLabel: 'Skip To Content',
59
60            // Menu labels and messages
61            menuLabel: 'Landmarks and Headings',
62            landmarkGroupLabel: 'Landmarks',
63            headingGroupLabel: 'Headings',
64            mofnGroupLabel: ' ($m of $n)',
65            headingLevelLabel: 'Heading level',
66            mainLabel: 'main',
67            searchLabel: 'search',
68            navLabel: 'navigation',
69            regionLabel: 'region',
70            asideLabel: 'complementary',
71            footerLabel: 'contentinfo',
72            headerLabel: 'banner',
73            formLabel: 'form',
74            msgNoLandmarksFound: 'No landmarks found',
75            msgNoHeadingsFound: 'No headings found',
76
77            // Action labels and messages
78            actionGroupLabel: 'Actions',
79            actionShowHeadingsHelp: 'Toggles between showing "All" and "Selected" Headings.',
80            actionShowSelectedHeadingsLabel: 'Show Selected Headings ($num)',
81            actionShowAllHeadingsLabel: 'Show All Headings ($num)',
82            actionShowLandmarksHelp: 'Toggles between showing "All" and "Selected" Landmarks.',
83            actionShowSelectedLandmarksLabel: 'Show Selected Landmarks ($num)',
84            actionShowAllLandmarksLabel: 'Show All Landmarks ($num)',
85
86            actionShowSelectedHeadingsAriaLabel: 'Show $num selected headings',
87            actionShowAllHeadingsAriaLabel: 'Show all $num headings',
88            actionShowSelectedLandmarksAriaLabel: 'Show $num selected landmarks',
89            actionShowAllLandmarksAriaLabel: 'Show all $num landmarks',
90
91            // Selectors for landmark and headings sections
92            landmarks: 'main, [role="main"], [role="search"], nav, [role="navigation"], aside, [role="complementary"]',
93            headings: 'main h1, [role="main"] h1, main h2, [role="main"] h2',
94
95            // Custom CSS position and colors
96            colorTheme: '',
97            fontFamily: '',
98            fontSize: '',
99            positionLeft: '',
100            menuTextColor: '',
101            menuBackgroundColor: '',
102            menuitemFocusTextColor: '',
103            menuitemFocusBackgroundColor: '',
104            focusBorderColor: '',
105            buttonTextColor: '',
106            buttonBackgroundColor: '',
107        },
108        colorThemes: {
109            'default': {
110                fontFamily: 'inherit',
111                fontSize: 'inherit',
112                positionLeft: '46%',
113                menuTextColor: '#1a1a1a',
114                menuBackgroundColor: '#dcdcdc',
115                menuitemFocusTextColor: '#eeeeee',
116                menuitemFocusBackgroundColor: '#1a1a1a',
117                focusBorderColor: '#1a1a1a',
118                buttonTextColor: '#1a1a1a',
119                buttonBackgroundColor: '#eeeeee',
120            },
121            'illinois': {
122                fontFamily: 'inherit',
123                fontSize: 'inherit',
124                positionLeft: '46%',
125                menuTextColor: '#00132c',
126                menuBackgroundColor: '#cad9ef',
127                menuitemFocusTextColor: '#eeeeee',
128                menuitemFocusBackgroundColor: '#00132c',
129                focusBorderColor: '#ff552e',
130                buttonTextColor: '#444444',
131                buttonBackgroundColor: '#dddede',
132            },
133            'aria': {
134                fontFamily: 'sans-serif',
135                fontSize: '10pt',
136                positionLeft: '7%',
137                menuTextColor: '#000',
138                menuBackgroundColor: '#def',
139                menuitemFocusTextColor: '#fff',
140                menuitemFocusBackgroundColor: '#005a9c',
141                focusBorderColor: '#005a9c',
142                buttonTextColor: '#005a9c',
143                buttonBackgroundColor: '#ddd',
144            }
145        },
146        defaultCSS: '.skip-to.popup{position:absolute;top:-30em;left:0}.skip-to,.skip-to.popup.focus{position:absolute;top:0;left:$positionLeft;font-family:$fontFamily;font-size:$fontSize}.skip-to.fixed{position:fixed}.skip-to button{position:relative;margin:0;padding:6px 8px 6px 8px;border-width:0 1px 1px 1px;border-style:solid;border-radius:0 0 6px 6px;border-color:$buttonBackgroundColor;color:$menuTextColor;background-color:$buttonBackgroundColor;z-index:200;font-family:$fontFamily;font-size:$fontSize}.skip-to .skip-to-tooltip{position:absolute;top:2.25em;left:8em;margin:1px;padding:4px;border:1px solid #ccc;box-shadow:2px 3px 5px #ddd;background-color:#eee;color:#000;font-family:Helvetica,Arial,Sans-Serif;font-variant-numeric:slashed-zero;font-size:9pt;width:auto;display:none;white-space:nowrap;z-index:201}.skip-to .skip-to-tooltip.skip-to-show-tooltip{display:block}.skip-to [aria-expanded=true]+.skip-to-tooltip.skip-to-show-tooltip{display:none}.skip-to [role=menu]{position:absolute;min-width:17em;display:none;margin:0;padding:.25rem;background-color:$menuBackgroundColor;border-width:2px;border-style:solid;border-color:$focusBorderColor;border-radius:5px;z-index:1000}.skip-to [role=group]{display:grid;grid-auto-rows:min-content;grid-row-gap:1px}.skip-to [role=separator]:first-child{border-radius:5px 5px 0 0}.skip-to [role=menuitem]{padding:3px;width:auto;border-width:0;border-style:solid;color:$menuTextColor;background-color:$menuBackgroundColor;z-index:1000;display:grid;overflow-y:auto;grid-template-columns:repeat(6,1.2rem) 1fr;grid-column-gap:2px;font-size:1em}.skip-to [role=menuitem] .label,.skip-to [role=menuitem] .level{font-size:100%;font-weight:400;color:$menuTextColor;display:inline-block;background-color:$menuBackgroundColor;line-height:inherit;display:inline-block}.skip-to [role=menuitem] .level{text-align:right;padding-right:4px}.skip-to [role=menuitem] .label{text-align:left;margin:0;padding:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[dir=rtl] .skip-to [role=menuitem] .label{text-align:right}.skip-to [role=menuitem] .label:first-letter,.skip-to [role=menuitem] .level:first-letter{text-decoration:underline;text-transform:uppercase}.skip-to [role=menuitem].skip-to-h1 .level{grid-column:1}.skip-to [role=menuitem].skip-to-h2 .level{grid-column:2}.skip-to [role=menuitem].skip-to-h3 .level{grid-column:3}.skip-to [role=menuitem].skip-to-h4 .level{grid-column:4}.skip-to [role=menuitem].skip-to-h5 .level{grid-column:5}.skip-to [role=menuitem].skip-to-h6 .level{grid-column:8}.skip-to [role=menuitem].skip-to-h1 .label{grid-column:2/8}.skip-to [role=menuitem].skip-to-h2 .label{grid-column:3/8}.skip-to [role=menuitem].skip-to-h3 .label{grid-column:4/8}.skip-to [role=menuitem].skip-to-h4 .label{grid-column:5/8}.skip-to [role=menuitem].skip-to-h5 .label{grid-column:6/8}.skip-to [role=menuitem].skip-to-h6 .label{grid-column:7/8}.skip-to [role=menuitem].skip-to-h1.no-level .label{grid-column:1/8}.skip-to [role=menuitem].skip-to-h2.no-level .label{grid-column:2/8}.skip-to [role=menuitem].skip-to-h3.no-level .label{grid-column:3/8}.skip-to [role=menuitem].skip-to-h4.no-level .label{grid-column:4/8}.skip-to [role=menuitem].skip-to-h5.no-level .label{grid-column:5/8}.skip-to [role=menuitem].skip-to-h6.no-level .label{grid-column:6/8}.skip-to [role=menuitem].skip-to-nesting-level-1 .nesting{grid-column:1}.skip-to [role=menuitem].skip-to-nesting-level-2 .nesting{grid-column:2}.skip-to [role=menuitem].skip-to-nesting-level-3 .nesting{grid-column:3}.skip-to [role=menuitem].skip-to-nesting-level-0 .label{grid-column:1/8}.skip-to [role=menuitem].skip-to-nesting-level-1 .label{grid-column:2/8}.skip-to [role=menuitem].skip-to-nesting-level-2 .label{grid-column:3/8}.skip-to [role=menuitem].skip-to-nesting-level-3 .label{grid-column:4/8}.skip-to [role=menuitem].action .label,.skip-to [role=menuitem].no-items .label{grid-column:1/8}.skip-to [role=separator]{margin:1px 0 1px 0;padding:3px;display:block;width:auto;font-weight:700;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:$menuTextColor;
146background-color:$menuBackgroundColor;color:$menuTextColor;z-index:1000}.skip-to [role=separator] .mofn{font-weight:400;font-size:85%}.skip-to [role=separator]:first-child{border-radius:5px 5px 0 0}.skip-to [role=menuitem].last{border-radius:0 0 5px 5px}.skip-to.focus{display:block}.skip-to button:focus,.skip-to button:hover{background-color:$menuBackgroundColor;color:$menuTextColor;outline:0}.skip-to button:focus{padding:6px 7px 5px 7px;border-width:0 2px 2px 2px;border-color:$focusBorderColor}.skip-to [role=menuitem]:focus{padding:1px;border-width:2px;border-style:solid;border-color:$focusBorderColor;background-color:$menuitemFocusBackgroundColor;color:$menuitemFocusTextColor;outline:0}.skip-to [role=menuitem]:focus .label,.skip-to [role=menuitem]:focus .level{background-color:$menuitemFocusBackgroundColor;color:$menuitemFocusTextColor}',
147
148        //
149        // Functions related to configuring the features
150        // of skipTo
151        //
152        isNotEmptyString: function(str) {
153            return (typeof str === 'string') && str.length && str.trim() && str !== "&nbsp;";
154        },
155        isEmptyString: function(str) {
156            return (typeof str !== 'string') || str.length === 0 && !str.trim();
157        },
158        init: function(config) {
159            var node;
160            // Check if skipto is already loaded
161
162            if (document.querySelector('style#' + this.skipToId)) {
163                return;
164            }
165
166            var attachElement = document.body;
167            if (config) {
168                this.setUpConfig(config);
169            }
170            if (typeof this.config.attachElement === 'string') {
171                node = document.querySelector(this.config.attachElement);
172                if (node && node.nodeType === Node.ELEMENT_NODE) {
173                    attachElement = node;
174                }
175            }
176            this.addCSSColors();
177            this.renderStyleElement(this.defaultCSS);
178            var elem = this.config.containerElement.toLowerCase().trim();
179            if (!this.isNotEmptyString(elem)) {
180                elem = 'div';
181            }
182            this.domNode = document.createElement(elem);
183            this.domNode.classList.add('skip-to');
184            if (this.isNotEmptyString(this.config.customClass)) {
185                this.domNode.classList.add(this.config.customClass);
186            }
187            if (this.isNotEmptyString(this.config.containerRole)) {
188                this.domNode.setAttribute('role', this.config.containerRole);
189            }
190            var displayOption = this.config.displayOption;
191            if (typeof displayOption === 'string') {
192                displayOption = displayOption.trim().toLowerCase();
193                if (displayOption.length) {
194                    switch (this.config.displayOption) {
195                        case 'fixed':
196                            this.domNode.classList.add('fixed');
197                            break;
198                        case 'onfocus':  // Legacy option
199                        case 'popup':
200                            this.domNode.classList.add('popup');
201                            break;
202                        default:
203                            break;
204                    }
205                }
206            }
207            // Place skip to at the beginning of the document
208            if (attachElement.firstElementChild) {
209                attachElement.insertBefore(this.domNode, attachElement.firstElementChild);
210            } else {
211                attachElement.appendChild(this.domNode);
212            }
213            this.buttonNode = document.createElement('button');
214            this.buttonNode.textContent = this.config.buttonLabel;
215            this.buttonNode.setAttribute('aria-haspopup', 'true');
216            this.buttonNode.setAttribute('aria-expanded', 'false');
217            this.buttonNode.setAttribute('aria-controls', this.skipToMenuId);
218
219            if (this.config.accesskey) {
220                this.buttonNode.setAttribute('accesskey', this.config.accesskey);
221            }
222
223            this.domNode.appendChild(this.buttonNode);
224
225            this.renderTooltip(this.domNode, this.buttonNode);
226
227            this.menuNode = document.createElement('div');
228            this.menuNode.setAttribute('role', 'menu');
229            this.menuNode.setAttribute('aria-busy', 'true');
230            this.menuNode.setAttribute('id', this.skipToMenuId);
231            this.domNode.appendChild(this.menuNode);
232            this.buttonNode.addEventListener('keydown', this.handleButtonKeydown.bind(this));
233            this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this));
234            this.buttonNode.addEventListener('focus', this.handleButtonFocus.bind(this));
235            this.buttonNode.addEventListener('blur', this.handleButtonBlur.bind(this));
236            this.buttonNode.addEventListener('pointerenter', this.handleButtonPointerenter.bind(this));
237            this.buttonNode.addEventListener('pointerout', this.handleButtonPointerout.b
237ind(this));
238            this.domNode.addEventListener('focusin', this.handleFocusin.bind(this));
239            this.domNode.addEventListener('focusout', this.handleFocusout.bind(this));
240            window.addEventListener('pointerdown', this.handleBackgroundPointerdown.bind(this), true);
241
242        },
243        renderTooltip: function(attachNode, buttonNode) {
244            var id = 'id-skip-to-tooltip';
245            var accesskey = this.getBrowserSpecificAccesskey(this.config.accesskey);
246
247            var tooltip = this.config.buttonTooltip;
248            // for backward compatibility, support 'this.config.buttonTitle' if defined
249            if (this.isNotEmptyString(this.config.buttonTitle)) {
250                tooltip = this.config.buttonTitle;
251            }
252
253            this.tooltipLeft = buttonNode.getBoundingClientRect().width;
254            this.tooltipTop  = buttonNode.getBoundingClientRect().height;
255
256            this.tooltipNode = document.createElement('div');
257            this.tooltipNode.setAttribute('role', 'tooltip');
258            this.tooltipNode.id = id;
259            this.tooltipNode.classList.add('skip-to-tooltip');
260
261            if (this.isNotEmptyString(accesskey)) {
262                tooltip = this.config.buttonTooltipAccesskey.replace('$key', accesskey);
263                // for backward compatibility support 'buttonTitleWithAccesskey' if defined
264                if (this.isNotEmptyString(this.config.buttonTitleWithAccesskey)) {
265                    tooltip = this.config.buttonTitleWithAccesskey.replace('$key', accesskey);
266                }
267            }
268
269            if (this.isEmptyString(tooltip)) {
270                // if there is no tooltip information
271                // do not display tooltip
272                this.config.enableTooltip = false;
273            } else {
274                this.tooltipNode.textContent = tooltip;
275            }
276
277            attachNode.appendChild(this.tooltipNode);
278            this.tooltipNode.style.left = this.tooltipLeft + 'px';
279            this.tooltipNode.style.top = this.tooltipTop + 'px';
280
281            // Temporarily show the tooltip to get rendered height
282            this.tooltipNode.classList.add('skip-to-show-tooltip');
283            this.tooltipHeight = this.tooltipNode.getBoundingClientRect().height;
284            this.tooltipNode.classList.remove('skip-to-show-tooltip');
285        },
286
287        updateStyle: function(stylePlaceholder, value, defaultValue) {
288            if (typeof value !== 'string' || value.length === 0) {
289                value = defaultValue;
290            }
291            var index1 = this.defaultCSS.indexOf(stylePlaceholder);
292            var index2 = index1 + stylePlaceholder.length;
293            while (index1 >= 0 && index2 < this.defaultCSS.length) {
294                this.defaultCSS = this.defaultCSS.substring(0, index1) + value + this.defaultCSS.substring(index2);
295                index1 = this.defaultCSS.indexOf(stylePlaceholder, index2);
296                index2 = index1 + stylePlaceholder.length;
297            }
298        },
299        addCSSColors: function() {
300            var theme = this.colorThemes['default'];
301            if (typeof this.colorThemes[this.config.colorTheme] === 'object') {
302                theme = this.colorThemes[this.config.colorTheme];
303            }
304            this.updateStyle('$fontFamily', this.config.fontFamily, theme.fontFamily);
305            this.updateStyle('$fontSize', this.config.fontSize, theme.fontSize);
306
307            this.updateStyle('$positionLeft', this.config.positionLeft, theme.positionLeft);
308
309            this.updateStyle('$menuTextColor', this.config.menuTextColor, theme.menuTextColor);
310            this.updateStyle('$menuBackgroundColor', this.config.menuBackgroundColor, theme.menuBackgroundColor);
311
312            this.updateStyle('$menuitemFocusTextColor', this.config.menuitemFocusTextColor, theme.menuitemFocusTextColor);
313            this.updateStyle('$menuitemFocusBackgroundColor', this.config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor);
314
315            this.updateStyle('$focusBorderColor', this.config.focusBorderColor, theme.focusBorderColor);
316
317            this.updateStyle('$buttonTextColor', this.config.buttonTextColor, theme.buttonTextColor);
318            this.updateStyle('$buttonBackgroundColor', this.config.buttonBackgroundColor, theme.buttonBackgroundColor);
319        },
320
321        getBrowserSpecificAccesskey: function (accesskey) {
322            var userAgent = navigator.userAgent.toLowerCase();
323            var platform =  navigator.platform.toLowerCase();
324
325            var hasWin    = platform.indexOf('win') >= 0;
326            var hasMac    = platform.indexOf('mac') >= 0;
327            var hasLinux  = platform.indexOf('linux') >= 0 || platform.indexOf('bsd') >= 0;
328
329            var hasAndroid = userAgent.indexOf('android') >= 0;
330            var hasFirefox = userAgent.indexOf('firefox') >= 0;
331            var hasChrome = userAgent.indexOf('chrome') >= 0;
332            var hasOpera = userAgent.indexOf('opr') >= 0;
333
334            if (typeof accesskey !== 'string' || accesskey.length === 0) {
335                return '';
336            }
337
338            if (hasWin || (hasLinux && !hasAndroid)) {
339                if (hasFirefox) {
340                    return "Shift + Alt + " + accesskey;
341                } else {
342                    if (hasChrome || hasOpera) {
343                        return "Alt + " + accesskey;
344                    }
345                }
346            }
347
348            if (hasMac) {
349                return "Ctrl + Option + " + accesskey;
350            }
351
352            return '';
353        },
354        setUpConfig: function(appConfig) {
355            var localConfig = this.config,
356                name,
357                appConfigSettings = typeof appConfig.settings !== 'undefined' ? appConfig.settings.skipTo : {};
358            for (name in appConfigSettings) {
359                //overwrite values of our local config, based on the external config
360                if ((typeof localConfig[name] !== 'undefined') &&
361                    ((typeof appConfigSettings[name] === 'string') &&
362                        (appConfigSettings[name].length > 0 ) ||
363                        typeof appConfigSettings[name] === 'boolean')
364                ) {
365                    localConfig[name] = appConfigSettings[name];
366                } else {
367                    throw new Error('** SkipTo Problem with user configuration option "' + name + '".');
368                }
369            }
370        },
371        renderStyleElement: function(cssString) {
372            var styleNode = document.createElement('style');
373            var headNode = document.getElementsByTagName('head')[0];
374            var css = document.createTextNode(cssString);
375
376            styleNode.setAttribute("type", "text/css");
377            // ID is used to test whether skipto is already loaded
378            styleNode.id = this.skipToId;
379            styleNode.appendChild(css);
380            headNode.appendChild(styleNode);
381        },
382
383        //
384        // Functions related to creating and populating the
385        // the popup menu
386        //
387
388        getFirstChar: function(menuitem) {
389            var c = '';
390            var label = menuitem.querySelector('.label');
391            if (label && this.isNotEmptyString(label.textContent)) {
392                c = label.textContent.trim()[0].toLowerCase();
393            }
394            return c;
395        },
396
397        getHeadingLevelFromAttribute: function(menuitem) {
398            var level = '';
399            if (menuitem.hasAttribute('data-level')) {
400                level = menuitem.getAttribute('data-level');
401            }
402            return level;
403        },
404
405        updateKeyboardShortCuts: function () {
406            var mi;
407            this.firstChars = [];
408            this.headingLevels = [];
409
410            for(var i = 0; i < this.menuitemNodes.length; i += 1) {
411                mi = this.menuitemNodes[i];
412                this.firstChars.push(this.getFirstChar(mi));
413                this.headingLevels.push(this.getHeadingLevelFromAttribute(mi));
414            }
415        },
416
417        updateMenuitems: function () {
418            var menuitemNodes = this.menuNode.querySelectorAll('[role=menuitem');
419
420            this.menuitemNodes = [];
421            for(var i = 0; i < menuitemNodes.length; i += 1) {
422                this.menuitemNodes.push(menuitemNodes[i]);
423            }
424
425            this.firstMenuitem = this.menuitemNodes[0];
426            this.lastMenuitem = this.menuitemNodes[this.menuitemNodes.length-1];
427            this.lastMenuitem.classList.add('last');
428            this.updateKeyboardShortCuts();
429        },
430
431        renderMenuitemToGroup: function (groupNode, mi) {
432            var tagNode, tagNodeChild, labelNode, nestingNode;
433
434            var menuitemNode = document.createElement('div');
435            menuitemNode.setAttribute('role', 'menuitem');
436            menuitemNode.classList.add(mi.class);
437            if (this.isNotEmptyString(mi.tagName)) {
438                menuitemNode.classList.add('skip-to-' + mi.tagName.toLowerCase());
439            }
440            menuitemNode.setAttribute('data-id', mi.dataId);
441            menuitemNode.tabIndex = -1;
442            if (this.isNotEmptyString(mi.ariaLabel)) {
443                menuitemNode.setAttribute('aria-label', mi.ariaLabel);
444            }
445
446            // add event handlers
447            menuitemNode.addEventListener('keydown', this.handleMenuitemKeydown.bind(this));
448            menuitemNode.addEventListener('click', this.handleMenuitemClick.bind(this));
449            menuitemNode.addEventListener('pointerenter', this.handleMenuitemPointerenter.bind(this));
450
451            groupNode.appendChild(menuitemNode);
452
453            // add heading level and label
454            if (mi.class.includes('heading')) {
455                if (this.config.enableHeadingLevelShortcuts) {
456                    tagNode = document.createElement('span');
457                    tagNodeChild = document.createElement('span');
458                    tagNodeChild.appendChild(document.createTextNode(mi.level));
459                    tagNode.append(tagNodeChild);
460                    tagNode.appendChild(document.createTextNode(')'));
461                    tagNode.classList.add('level');
462                    menuitemNode.append(tagNode);
463                } else {
464                    menuitemNode.classList.add('no-level');
465                }
466                menuitemNode.setAttribute('data-level', mi.level);
467                if (this.isNotEmptyString(mi.tagName)) {
468                    menuitemNode.classList.add('skip-to-' + mi.tagName);
469                }
470            }
471
472            // add nesting level for landmarks
473            if (mi.class.includes('landmark')) {
474                menuitemNode.setAttribute('data-nesting', mi.nestingLevel);
475                menuitemNode.classList.add('skip-to-nesting-level-' + mi.nestingLevel);
476
477                if (mi.nestingLevel > 0 && (mi.nestingLevel > this.lastNestingLevel)) {
478                    nestingNode = document.createElement('span');
479                    nestingNode.classList.add('nesting');
480                    menuitemNode.append(nestingNode);
481                }
482                this.lastNestingLevel = mi.nestingLevel;
483            }
484
485            labelNode = document.createElement('span');
486            labelNode.appendChild(document.createTextNode(mi.name));
487            labelNode.classList.add('label');
488            menuitemNode.append(labelNode);
489
490            return menuitemNode;
491        },
492
493        renderGroupLabel: function (groupLabelId, title, m, n) {
494            var titleNode, mofnNode, s;
495            var groupLabelNode = document.getElementById(groupLabelId);
496
497            titleNode = groupLabelNode.querySelector('.title');
498            mofnNode = groupLabelNode.querySelector('.mofn');
499
500            titleNode.textContent = title;
501
502            if (this.config.enableActions && this.config.enableMofN) {
503                if ((typeof m === 'number') && (typeof n === 'number')) {
504                    s = this.config.mofnGroupLabel;
505                    s = s.replace('$m', m);
506                    s = s.replace('$n', n);
507                    mofnNode.textContent = s;
508                }
509            }
510        },
511
512        renderMenuitemGroup: function(groupId, title) {
513            var labelNode, groupNode, spanNode;
514            var menuNode = this.menuNode;
515            if (this.isNotEmptyString(title)) {
516                labelNode = document.createElement('div');
517                labelNode.id = groupId + "-label";
518                labelNode.setAttribute('role', 'separator');
519                menuNode.appendChild(labelNode);
520
521                spanNode = document.createElement('span');
522                spanNode.classList.add('title');
523                spanNode.textContent = title;
524                labelNode.append(spanNode);
525
526                spanNode = document.createElement('span');
527                spanNode.classList.add('mofn');
528                labelNode.append(spanNode);
529
530                groupNode = document.createElement('div');
531                groupNode.setAttribute('role', 'group');
532                groupNode.setAttribute('aria-labelledby', labelNode.id);
533                groupNode.id = groupId;
534                menuNode.appendChild(groupNode);
535                menuNode = groupNode;
536            }
537            return groupNode;
538        },
539
540        removeMenuitemGroup: function(groupId) {
541            var node = document.getElementById(groupId);
542            this.menuNode.removeChild(node);
543            node = document.getElementById(groupId + "-label");
544            this.menuNode.removeChild(node);
545        },
546
547        renderMenuitemsToGroup: function(groupNode, menuitems, msgNoItemsFound) {
548            groupNode.innerHTML = '';
549            this.lastNestingLevel = 0;
550
551            if (menuitems.length === 0) {
552                var item = {};
553                item.name = msgNoItemsFound;
554                item.tagName = '';
555                item.class = 'no-items';
556                item.dataId = '';
557                this.renderMenuitemToGroup(groupNode, item);
558            }
559            else {
560                for (var i = 0; i < menuitems.length; i += 1) {
561                    this.renderMenuitemToGroup(groupNode, menuitems[i]);
562                }
563            }
564        },
565
566        getShowMoreHeadingsSelector: function(option) {
567            if (option === 'all') {
568                return this.showAllHeadingsSelector;
569            }
570            return this.config.headings;
571        },
572
573        getShowMoreHeadingsLabel: function(option, n) {
574            var label = this.config.actionShowSelectedHeadingsLabel;
575            if (option === 'all') {
576                label = this.config.actionShowAllHeadingsLabel;
577            }
578            return label.replace('$num', n);
579        },
580
581        getShowMoreHeadingsAriaLabel: function(option, n) {
582            var label = this.config.actionShowSelectedHeadingsAriaLabel;
583
584            if (option === 'all') {
585                label = this.config.actionShowAllHeadingsAriaLabel;
586            }
587
588            return label.replace('$num', n);
589        },
590
591        renderActionMoreHeadings: function(groupNode) {
592            var item, menuitemNode;
593            var option = 'all';
594
595            var selectedHeadingsLen = this.getHeadings(this.getShowMoreHeadingsSelector('selected')).length;
596            var allHeadingsLen = this.getHeadings(this.getShowMoreHeadingsSelector('all')).length;
597            var noAction = selectedHeadingsLen === allHeadingsLen;
598            var headingsLen = allHeadingsLen;
599
600            if (option !== 'all') {
601                headingsLen = selectedHeadingsLen;
602            }
603
604            if (!noAction) {
605                item = {};
606                item.tagName = '';
607                item.role = 'menuitem';
608                item.class = 'action';
609                item.dataId = 'skip-to-more-headings';
610                item.name = this.getShowMoreHeadingsLabel(option, headingsLen);
611                item.ariaLabel = this.getShowMoreHeadingsAriaLabel(option, headingsLen);
612
613                menuitemNode = this.renderMenuitemToGroup(groupNode, item);
614                menuitemNode.setAttribute('data-show-heading-option', option);
615                menuitemNode.title = this.config.actionShowHeadingsHelp;
616            }
617            return noAction;
618        },
619
620        updateHeadingGroupMenuitems: function(option) {
621            var headings, headingsLen, labelNode, groupNode;
622
623            var selectedHeadings = this.getHeadings(this.getShowMoreHeadingsSelector('selected'));
624            var selectedHeadingsLen = selectedHeadings.length;
625            var allHeadings = this.getHeadings(this.getShowMoreHeadingsSelector('all'));
626            var allHeadingsLen = allHeadings.length;
627
628            // Update list of headings
629            if ( option === 'all' ) {
630                headings = allHeadings;
631            }
632            else {
633                headings = selectedHeadings;
634            }
635
636            this.renderGroupLabel('id-skip-to-group-headings-label', this.config.headingGroupLabel, headings.length, allHeadings.length);
637
638            groupNode = document.getElementById('id-skip-to-group-headings');
639            this.renderMenuitemsToGroup(groupNode, headings, this.config.msgNoHeadingsFound);
640            this.updateMenuitems();
641
642            // Move focus to first heading menuitem
643            if (groupNode.firstElementChild) {
644                groupNode.firstElementChild.focus();
645            }
646
647            // Update heading action menuitem
648            if (option === 'all') {
649                option = 'selected';
650                headingsLen = selectedHeadingsLen;
651            } else {
652                option = 'all';
653                headingsLen = allHeadingsLen;
654            }
655
656            var menuitemNode = this.menuNode.querySelector('[data-id=skip-to-more-headings]');
657            menuitemNode.setAttribute('data-show-heading-option', option);
658            menuitemNode.setAttribute('aria-label', this.getShowMoreHeadingsAriaLabel(option, headingsLen));
659
660            labelNode = menuitemNode.querySelector('span.label');
661            labelNode.textContent = this.getShowMoreHeadingsLabel(option, headingsLen);
662        },
663
664        getShowMoreLandmarksSelector: function(option) {
665            if (option === 'all') {
666                return this.showAllLandmarksSelector;
667            }
668            return this.config.landmarks;
669        },
670
671        getShowMoreLandmarksLabel: function(option, n) {
672            var label = this.config.actionShowSelectedLandmarksLabel;
673
674            if (option === 'all') {
675                label = this.config.actionShowAllLandmarksLabel;
676            }
677            return label.replace('$num', n);
678        },
679
680        getShowMoreLandmarksAriaLabel: function(option, n) {
681            var label = this.config.actionShowSelectedLandmarksAriaLabel;
682
683            if (option === 'all') {
684                label = this.config.actionShowAllLandmarksAriaLabel;
685            }
686
687            return label.replace('$num', n);
688        },
689
690        renderActionMoreLandmarks: function(groupNode) {
691            var item, menuitemNode;
692            var option = 'all';
693
694            var selectedLandmarksLen = this.getLandmarks(this.getShowMoreLandmarksSelector('selected')).length;
695            var allLandmarksLen = this.getLandmarks(this.getShowMoreLandmarksSelector('all')).length;
696            var noAction = selectedLandmarksLen === allLandmarksLen;
697            var landmarksLen = allLandmarksLen;
698
699            if (option !== 'all') {
700                landmarksLen = selectedLandmarksLen;
701            }
702
703            if (!noAction) {
704                item = {};
705                item.tagName = '';
706                item.role = 'menuitem';
707                item.class = 'action';
708                item.dataId = 'skip-to-more-landmarks';
709                item.name = this.getShowMoreLandmarksLabel(option, landmarksLen);
710                item.ariaLabel =  this.getShowMoreLandmarksAriaLabel(option, landmarksLen);
711
712                menuitemNode = this.renderMenuitemToGroup(groupNode, item);
713
714                menuitemNode.setAttribute('data-show-landmark-option', option);
715                menuitemNode.title = this.config.actionShowLandmarksHelp;
716            }
717            return noAction;
718        },
719
720        updateLandmarksGroupMenuitems: function(option) {
721            var landmarks, landmarksLen, labelNode, groupNode;
722            var selectedLandmarks = this.getLandmarks(this.getShowMoreLandmarksSelector('selected'));
723            var selectedLandmarksLen = selectedLandmarks.length;
724            var allLandmarks = this.getLandmarks(this.getShowMoreLandmarksSelector('all'), true);
725            var allLandmarksLen = allLandmarks.length;
726
727            // Update landmark menu items
728            if ( option === 'all' ) {
729                landmarks = allLandmarks;
730            }
731            else {
732                landmarks = selectedLandmarks;
733            }
734
735            this.renderGroupLabel('id-skip-to-group-landmarks-label', this.config.landmarkGroupLabel, landmarks.length, allLandmarks.length);
736
737            groupNode = document.getElementById('id-skip-to-group-landmarks');
738            this.renderMenuitemsToGroup(groupNode, landmarks, this.config.msgNoLandmarksFound);
739            this.updateMenuitems();
740
741            // Move focus to first landmark menuitem
742            if (groupNode.firstElementChild) {
743                groupNode.firstElementChild.focus();
744            }
745
746            // Update landmark action menuitem
747            if (option === 'all') {
748                option = 'selected';
749                landmarksLen = selectedLandmarksLen;
750            } else {
751                option = 'all';
752                landmarksLen = allLandmarksLen;
753            }
754
755            var menuitemNode = this.menuNode.querySelector('[data-id=skip-to-more-landmarks]');
756            menuitemNode.setAttribute('data-show-landmark-option', option);
757            menuitemNode.setAttribute('aria-label', this.getShowMoreLandmarksAriaLabel(option, landmarksLen));
758
759            labelNode = menuitemNode.querySelector('span.label');
760            labelNode.textContent = this.getShowMoreLandmarksLabel(option, landmarksLen);
761        },
762
763        renderMenu: function() {
764            var groupNode,
765                selectedLandmarks,
766                allLandmarks,
767                landmarkElements,
768                selectedHeadings,
769                allHeadings,
770                headingElements,
771                selector,
772                option,
773                hasNoAction1,
774                hasNoAction2;
775            // remove current menu items from menu
776            while (this.menuNode.lastElementChild) {
777                this.menuNode.removeChild(this.menuNode.lastElementChild);
778            }
779
780            option = 'selected';
781            // Create landmarks group
782            selector = this.getShowMoreLandmarksSelector('all');
783            allLandmarks = this.getLandmarks(selector, true);
784            selector = this.getShowMoreLandmarksSelector('selected');
785            selectedLandmarks = this.getLandmarks(selector);
786            landmarkElements = selectedLandmarks;
787
788            if (option === 'all') {
789                landmarkElements = allLandmarks;
790            }
791
792            groupNode = this.renderMenuitemGroup('id-skip-to-group-landmarks', this.config.landmarkGroupLabel);
793            this.renderMenuitemsToGroup(groupNode, landmarkElements, this.config.msgNoLandmarksFound);
794            this.renderGroupLabel('id-skip-to-group-landmarks-label', this.config.landmarkGroupLabel, landmarkElements.length, allLandmarks.length);
795
796            // Create headings group
797            selector = this.getShowMoreHeadingsSelector('all');
798            allHeadings = this.getHeadings(selector);
799            selector = this.getShowMoreHeadingsSelector('selected');
800            selectedHeadings = this.getHeadings(selector);
801            headingElements = selectedHeadings;
802
803            if (option === 'all') {
804                headingElements = allHeadings;
805            }
806
807            groupNode = this.renderMenuitemGroup('id-skip-to-group-headings', this.config.headingGroupLabel);
808            this.renderMenuitemsToGroup(groupNode, headingElements, this.config.msgNoHeadingsFound);
809            this.renderGroupLabel('id-skip-to-group-headings-label', this.config.headingGroupLabel, headingElements.length, allHeadings.length);
810
811            // Create actions, if enabled
812            if (this.config.enableActions) {
813                groupNode = this.renderMenuitemGroup('id-skip-to-group-actions', this.config.actionGroupLabel);
814                hasNoAction1 = this.renderActionMoreLandmarks(groupNode);
815                hasNoAction2 = this.renderActionMoreHeadings(groupNode);
816                // Remove action label if no actions are available
817                if (hasNoAction1 && hasNoAction2) {
818                    this.removeMenuitemGroup('id-skip-to-group-actions');
819                }
820            }
821
822            // Update list of menuitems
823            this.updateMenuitems();
824        },
825
826        //
827        // Menu scripting event functions and utilities
828        //
829
830        setFocusToMenuitem: function(menuitem) {
831            if (menuitem) {
832                menuitem.focus();
833            }
834        },
835
836        setFocusToFirstMenuitem: function() {
837            this.setFocusToMenuitem(this.firstMenuitem);
838        },
839
840        setFocusToLastMenuitem: function() {
841            this.setFocusToMenuitem(this.lastMenuitem);
842        },
843
844        setFocusToPreviousMenuitem: function(menuitem) {
845            var newMenuitem, index;
846            if (menuitem === this.firstMenuitem) {
847                newMenuitem = this.lastMenuitem;
848            } else {
849                index = this.menuitemNodes.indexOf(menuitem);
850                newMenuitem = this.menuitemNodes[index - 1];
851            }
852            this.setFocusToMenuitem(newMenuitem);
853            return newMenuitem;
854        },
855
856        setFocusToNextMenuitem: function(menuitem) {
857            var newMenuitem, index;
858            if (menuitem === this.lastMenuitem) {
859                newMenuitem = this.firstMenuitem;
860            } else {
861                index = this.menuitemNodes.indexOf(menuitem);
862                newMenuitem = this.menuitemNodes[index + 1];
863            }
864            this.setFocusToMenuitem(newMenuitem);
865            return newMenuitem;
866        },
867
868        setFocusByFirstCharacter: function(menuitem, char) {
869            var start, index;
870            if (char.length > 1) {
871                return;
872            }
873            char = char.toLowerCase();
874
875            // Get start index for search based on position of currentItem
876            start = this.menuitemNodes.indexOf(menuitem) + 1;
877            if (start >= this.menuitemNodes.length) {
878                start = 0;
879            }
880
881            // Check remaining items in the menu
882            index = this.firstChars.indexOf(char, start);
883
884            // If not found in remaining items, check headings
885            if (index === -1) {
886                index = this.headingLevels.indexOf(char, start);
887            }
888
889            // If not found in remaining items, check from beginning
890            if (index === -1) {
891                index = this.firstChars.indexOf(char, 0);
892            }
893
894            // If not found in remaining items, check headings from beginning
895            if (index === -1) {
896                index = this.headingLevels.indexOf(char, 0);
897            }
898
899            // If match was found...
900            if (index > -1) {
901                this.setFocusToMenuitem(this.menuitemNodes[index]);
902            }
903        },
904
905        // Utilities
906        getIndexFirstChars: function(startIndex, char) {
907            for (var i = startIndex; i < this.firstChars.length; i += 1) {
908                if (char === this.firstChars[i]) {
909                    return i;
910                }
911            }
912            return -1;
913        },
914        // Popup menu methods
915        openPopup: function() {
916            this.menuNode.setAttribute('aria-busy', 'true');
917            this.renderMenu();
918            this.menuNode.style.display = 'block';
919            this.menuNode.removeAttribute('aria-busy');
920            this.buttonNode.setAttribute('aria-expanded', 'true');
921        },
922
923        closePopup: function() {
924            if (this.isOpen()) {
925                this.buttonNode.setAttribute('aria-expanded', 'false');
926                this.menuNode.style.display = 'none';
927            }
928        },
929        isOpen: function() {
930            return this.buttonNode.getAttribute('aria-expanded') === 'true';
931        },
932        // Menu event handlers
933        handleFocusin: function() {
934            this.domNode.classList.add('focus');
935        },
936        handleFocusout: function() {
937            this.domNode.classList.remove('focus');
938        },
939        handleButtonKeydown: function(event) {
940            var key = event.key,
941                flag = false;
942            switch (key) {
943                case ' ':
944                case 'Enter':
945                case 'ArrowDown':
946                case 'Down':
947                    this.openPopup();
948                    this.setFocusToFirstMenuitem();
949                    flag = true;
950                    break;
951                case 'Esc':
952                case 'Escape':
953                    this.closePopup();
954                    this.buttonNode.focus();
955                    this.hideTooltip();
956                    flag = true;
957                    break;
958                case 'Up':
959                case 'ArrowUp':
960                    this.openPopup();
961                    this.setFocusToLastMenuitem();
962                    flag = true;
963                    break;
964                default:
965                    break;
966            }
967            if (flag) {
968                event.stopPropagation();
969                event.preventDefault();
970            }
971        },
972        handleButtonClick: function(event) {
973            if (this.isOpen()) {
974                this.closePopup();
975                this.buttonNode.focus();
976            } else {
977                this.openPopup();
978                this.setFocusToFirstMenuitem();
979            }
980            event.stopPropagation();
981            event.preventDefault();
982        },
983        isTooltipHidden: function() {
984            return this.tooltipNode.className.indexOf('skip-to-s
984how-tooltip') < 0;
985        },
986        displayTooltip: function() {
987            if (this.showTooltipFocus || this.showTooltipHover) {
988                this.tooltipNode.classList.add('skip-to-show-tooltip');
989            }
990        },
991        showTooltip: function() {
992            this.showTooltipFocus = true;
993            if (this.config.enableTooltip && this.isTooltipHidden()) {
994                this.tooltipNode.style.left = this.tooltipLeft + 'px';
995                this.tooltipNode.style.top = this.tooltipTop + 'px';
996                setTimeout(this.displayTooltip.bind(this), this.tooltipTimerDelay);
997            }
998        },
999        hideTooltip: function() {
1000            this.showTooltipFocus = false;
1001            if(this.config.enableTooltip) {
1002                this.tooltipNode.classList.remove('skip-to-show-tooltip');
1003            }
1004        },
1005        handleButtonFocus: function() {
1006            this.showTooltip();
1007        },
1008        handleButtonBlur: function() {
1009            this.hideTooltip();
1010        },
1011        handleButtonPointerenter: function(event) {
1012            this.showTooltipHover = true;
1013            if (this.config.enableTooltip && this.isTooltipHidden()) {
1014                var rect = this.buttonNode.getBoundingClientRect();
1015                var left = Math.min(this.tooltipLeft, event.pageX - rect.left + this.tooltipHeight);
1016                this.tooltipNode.style.left = left + 'px';
1017                var top = event.pageY - rect.top;
1018                this.tooltipNode.style.top = top + 'px';
1019                setTimeout(this.showTooltip. bind(this), this.tooltipTimerDelay);
1020            }
1021        },
1022        handleButtonPointerout: function() {
1023            this.showTooltipHover = false;
1024            if(this.config.enableTooltip) {
1025                this.tooltipNode.classList.remove('skip-to-show-tooltip');
1026            }
1027        },
1028        skipToElement: function(menuitem) {
1029
1030            var isVisible = this.isVisible;
1031            var focusNode = false;
1032            var scrollNode = false;
1033            var elem;
1034
1035            function findVisibleElement(e, selectors) {
1036                if (e) {
1037                    for (var j = 0; j < selectors.length; j += 1) {
1038                        var elems = e.querySelectorAll(selectors[j]);
1039                        for(var i = 0; i < elems.length; i +=1) {
1040                            if (isVisible(elems[i])) {
1041                                return elems[i];
1042                            }
1043                        }
1044                    }
1045                }
1046                return e;
1047            }
1048
1049            var searchSelectors = ['input', 'button', 'input[type=button]', 'input[type=submit]', 'a'];
1050            var navigationSelectors = ['a', 'input', 'button', 'input[type=button]', 'input[type=submit]'];
1051            var landmarkSelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'article', 'p', 'li', 'a'];
1052
1053            var isLandmark = menuitem.classList.contains('landmark');
1054            var isSearch = menuitem.classList.contains('skip-to-search');
1055            var isNav = menuitem.classList.contains('skip-to-nav');
1056
1057            elem = document.querySelector('[data-skip-to-id="' + menuitem.getAttribute('data-id') + '"]');
1058
1059            if (elem) {
1060                if (isSearch) {
1061                    focusNode = findVisibleElement(elem, searchSelectors);
1062                }
1063                if (isNav) {
1064                    focusNode = findVisibleElement(elem, navigationSelectors);
1065                }
1066                if (focusNode && this.isVisible(focusNode)) {
1067                    focusNode.focus();
1068                    focusNode.scrollIntoView({block: 'nearest'});
1069                }
1070                else {
1071                    if (isLandmark) {
1072                        scrollNode = findVisibleElement(elem, landmarkSelectors);
1073                        if (scrollNode) {
1074                            elem = scrollNode;
1075                        }
1076                    }
1077                    elem.tabIndex = -1;
1078                    elem.focus();
1079                    elem.scrollIntoView({block: 'center'});
1080                }
1081            }
1082        },
1083        handleMenuitemAction: function(tgt) {
1084            var option;
1085            switch (tgt.getAttribute('data-id')) {
1086                case '':
1087                    // this means there were no headings or landmarks in the list
1088                    break;
1089
1090                case 'skip-to-more-headings':
1091                    option = tgt.getAttribute('data-show-heading-option');
1092                    this.updateHeadingGroupMenuitems(option);
1093                    break;
1094
1095                case 'skip-to-more-landmarks':
1096                    option = tgt.getAttribute('data-show-landmark-option');
1097                    this.updateLandmarksGroupMenuitems(option);
1098                    break;
1099
1100                default:
1101                    this.closePopup();
1102                    this.skipToElement(tgt);
1103                    break;
1104            }
1105        },
1106        handleMenuitemKeydown: function(event) {
1107            var tgt = event.currentTarget,
1108                key = event.key,
1109                flag = false;
1110
1111            function isPrintableCharacter(str) {
1112                return str.length === 1 && str.match(/\S/);
1113            }
1114            if (event.ctrlKey || event.altKey || event.metaKey) {
1115                return;
1116            }
1117            if (event.shiftKey) {
1118                if (isPrintableCharacter(key)) {
1119                    this.setFocusByFirstCharacter(tgt, key);
1120                    flag = true;
1121                }
1122                if (event.key === 'Tab') {
1123                    this.buttonNode.focus();
1124                    this.closePopup();
1125                    flag = true;
1126                }
1127            } else {
1128                switch (key) {
1129                    case 'Enter':
1130                    case ' ':
1131                        this.handleMenuitemAction(tgt);
1132                        flag = true;
1133                        break;
1134                    case 'Esc':
1135                    case 'Escape':
1136                        this.closePopup();
1137                        this.buttonNode.focus();
1138                        flag = true;
1139                        break;
1140                    case 'Up':
1141                    case 'ArrowUp':
1142                        this.setFocusToPreviousMenuitem(tgt);
1143                        flag = true;
1144                        break;
1145                    case 'ArrowDown':
1146                    case 'Down':
1147                        this.setFocusToNextMenuitem(tgt);
1148                        flag = true;
1149                        break;
1150                    case 'Home':
1151                    case 'PageUp':
1152                        this.setFocusToFirstMenuitem();
1153                        flag = true;
1154                        break;
1155                    case 'End':
1156                    case 'PageDown':
1157                        this.setFocusToLastMenuitem();
1158                        flag = true;
1159                        break;
1160                    case 'Tab':
1161                        this.closePopup();
1162                        break;
1163                    default:
1164                        if (isPrintableCharacter(key)) {
1165                            this.setFocusByFirstCharacter(tgt, key);
1166                            flag = true;
1167                        }
1168                        break;
1169                }
1170            }
1171            if (flag) {
1172                event.stopPropagation();
1173                event.preventDefault();
1174            }
1175        },
1176        handleMenuitemClick: function(event) {
1177            this.handleMenuitemAction(event.currentTarget);
1178            event.stopPropagation();
1179            event.preventDefault();
1180        },
1181        handleMenuitemPointerenter: function(event) {
1182            var tgt = event.currentTarget;
1183            tgt.focus();
1184        },
1185        handleBackgroundPointerdown: function(event) {
1186            if (!this.domNode.contains(event.target)) {
1187                if (this.isOpen()) {
1188                    this.closePopup();
1189                    this.buttonNode.focus();
1190                }
1191            }
1192        },
1193        // methods to extract landmarks, headings and ids
1194        normalizeName: function(name) {
1195            if (typeof name === 'string') return name.replace(/\w\S*/g, function(txt) {
1196                return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
1197            });
1198            return "";
1199        },
1200        getTextContent: function(elem) {
1201            function getText(e, strings) {
1202                // If text node get the text and return
1203                if (e.nodeType === Node.TEXT_NODE) {
1204                    strings.push(e.data);
1205                } else {
1206                    // if an element for through all the children elements looking for text
1207                    if (e.nodeType === Node.ELEMENT_NODE) {
1208                        // check to see if IMG or AREA element and to use ALT content if defined
1209                        var tagName = e.tagName.toLowerCase();
1210                        if ((tagName === 'img') || (tagName === 'area')) {
1211                            if (e.alt) {
1212                                strings.push(e.alt);
1213                            }
1214                        } else {
1215                            var c = e.firstChild;
1216                            while (c) {
1217                                getText(c, strings);
1218                                c = c.nextSibling;
1219                            } // end loop
1220                        }
1221                    }
1222                }
1223            } // end function getStrings
1224            // Create return object
1225            var str = "Test",
1226                strings = [];
1227            getText(elem, strings);
1228            if (strings.length) str = strings.join(" ");
1229            return str;
1230        },
1231        getAccessibleName: function(elem) {
1232            var labelledbyIds = elem.getAttribute('aria-labelledby'),
1233                label = elem.getAttribute('aria-label'),
1234                title = elem.getAttribute('title'),
1235                name = "";
1236            if (labelledbyIds && labelledbyIds.length) {
1237                var str,
1238                    strings = [],
1239                    ids = labelledbyIds.split(' ');
1240                if (!ids.length) ids = [labelledbyIds];
1241                for (var i = 0, l = ids.length; i < l; i += 1) {
1242                    var e = document.getElementById(ids[i]);
1243                    if (e) str = this.getTextContent(e);
1244                    if (str && str.length) strings.push(str);
1245                }
1246                name = strings.join(" ");
1247            } else {
1248                if (this.isNotEmptyString(label)) {
1249                    name = label;
1250                } else {
1251                    if (this.isNotEmptyString(title)) {
1252                        name = title;
1253                    }
1254                }
1255            }
1256            return name;
1257        },
1258        isVisible: function(element) {
1259            function isVisibleRec(el) {
1260                if (el.nodeType === 9) return true; /*IE8 does not support Node.DOCUMENT_NODE*/
1261                var computedStyle = window.getComputedStyle(el);
1262                var display = computedStyle.getPropertyValue('display');
1263                var visibility = computedStyle.getPropertyValue('visibility');
1264                var hidden = el.getAttribute('hidden');
1265                if ((display === 'none') ||
1266                    (visibility === 'hidden') ||
1267                    (hidden !== null)) {
1268                    return false;
1269                }
1270                return isVisibleRec(el.parentNode);
1271            }
1272            return isVisibleRec(element);
1273        },
1274        getHeadings: function(targets) {
1275            var dataId, level;
1276            if (typeof targets !== 'string') {
1277                targets = this.config.headings;
1278            }
1279            var headingElementsArr = [];
1280            if (typeof targets !== 'string' || targets.length === 0) return;
1281            var headings = document.querySelectorAll(targets);
1282            for (var i = 0, len = headings.length; i < len; i += 1) {
1283                var heading = headings[i];
1284                var role = heading.getAttribute('role');
1285                if ((typeof role === 'string') && (role === 'presentation')) continue;
1286                if (this.isVisible(heading) && this.isNotEmptyString(heading.innerHTML)) {
1287                    if (heading.hasAttribute('data-skip-to-id')) {
1288                        dataId = heading.getAttribute('data-skip-to-id');
1289                    } else {
1290                        heading.setAttribute('data-skip-to-id', this.skipToIdIndex);
1291                        dataId = this.skipToIdIndex;
1292                    }
1293                    level = heading.tagName.substring(1);
1294                    var headingItem = {};
1295                    headingItem.dataId = dataId.toString();
1296                    headingItem.class = 'heading';
1297                    headingItem.name = this.getTextContent(heading);
1298                    headingItem.ariaLabel = headingItem.name + ', ';
1299                    headingItem.ariaLabel += this.config.headingLevelLabel + ' ' + level;
1300                    headingItem.tagName = heading.tagName.toLowerCase();
1301                    headingItem.role = 'heading';
1302                    headingItem.level = level;
1303                    headingElementsArr.push(headingItem);
1304                    this.skipToIdIndex += 1;
1305                }
1306            }
1307            return headingElementsArr;
1308        },
1309        getLocalizedLandmarkName: function(tagName, name) {
1310            var n;
1311            switch (tagName) {
1312                case 'aside':
1313                    n = this.config.asideLabel;
1314                    break;
1315                case 'footer':
1316                    n = this.config.footerLabel;
1317                    break;
1318                case 'form':
1319                    n = this.config.formLabel;
1320                    break;
1321                case 'header':
1322                    n = this.config.headerLabel;
1323                    break;
1324                case 'main':
1325                    n = this.config.mainLabel;
1326                    break;
1327                case 'nav':
1328                    n = this.config.navLabel;
1329                    break;
1330                case 'section':
1331                case 'region':
1332                    n = this.config.regionLabel;
1333                    break;
1334                case 'search':
1335                    n = this.config.searchLabel;
1336                    break;
1337                // When an ID is used as a selector, assume for main content
1338                default:
1339                    n = tagName;
1340                    break;
1341            }
1342            if (this.isNotEmptyString(name)) {
1343                n += ': ' + name;
1344            }
1345            return n;
1346        },
1347        getNestingLevel: function(landmark, landmarks) {
1348            var nestingLevel = 0;
1349            var parentNode = landmark.parentNode;
1350            while (parentNode) {
1351                for (var i = 0; i < landmarks.length; i += 1) {
1352                    if (landmarks[i] === parentNode) {
1353                        nestingLevel += 1;
1354                        // no more than 3 levels of nesting supported
1355                        if (nestingLevel === 3) {
1356                            return 3;
1357                        }
1358                        continue;
1359                    }
1360                }
1361                parentNode = parentNode.parentNode;
1362            }
1363            return nestingLevel;
1364        },
1365        getLandmarks: function(targets, allFlag) {
1366            if (typeof allFlag !== 'boolean') {
1367                allFlag = false;
1368            }
1369            if (typeof targets !== 'string') {
1370                targets = this.config.landmarks;
1371            }
1372            var landmarks = document.querySelectorAll(targets);
1373            var mainElements = [];
1374            var searchElements = [];
1375            var navElements = [];
1376            var asideElements = [];
1377            var footerElements = [];
1378            var regionElements = [];
1379            var otherElements = [];
1380            var allLandmarks = [];
1381            var dataId = '';
1382            for (var i = 0, len = landmarks.length; i < len; i += 1) {
1383                var landmark = landmarks[i];
1384                // if skipto is a landmark don't include it in the list
1385                if (landmark === this.domNode) {
1386                    continue;
1387                }
1388                var role = landmark.getAttribute('role');
1389                var tagName = landmark.tagName.toLowerCase();
1390                if ((typeof role === 'string') && (role === 'presentation')) continue;
1391                if (this.isVisible(landmark)) {
1392                    if (!role) role = tagName;
1393                    var name = this.getAccessibleName(landmark);
1394                    if (typeof name !== 'string') {
1395                        name = '';
1396                    }
1397                    // normalize tagNames
1398                    switch (role) {
1399                        case 'banner':
1400                            tagName = 'header';
1401                            break;
1402                        case 'complementary':
1403                            tagName = 'aside';
1404                            break;
1405                        case 'contentinfo':
1406                            tagName = 'footer';
1407                            break;
1408                        case 'form':
1409                            tagName = 'form';
1410                            break;
1411                        case 'main':
1412                            tagName = 'main';
1413                            break;
1414                        case 'navigation':
1415                            tagName = 'nav';
1416                            break;
1417                        case 'region':
1418                            tagName = 'section';
1419                            break;
1420                        case 'search':
1421                            tagName = 'search';
1422                            break;
1423                        default:
1424                            break;
1425                    }
1426                    // if using ID for selectQuery give tagName as main
1427                    if (['aside', 'footer', 'form', 'header', 'main', 'nav', 'section', 'search'].indexOf(tagName) < 0) {
1428                        tagName = 'main';
1429                    }
1430                    if (landmark.hasAttribute('aria-roledescription')) {
1431                        tagName = landmark.getAttribute('aria-roledescription').trim().replace(' ', '-');
1432                    }
1433                    if (landmark.hasAttribute('data-skip-to-id')) {
1434                        dataId = landmark.getAttribute('data-skip-to-id');
1435                    } else {
1436                        landmark.setAttribute('data-skip-to-id', this.skipToIdIndex);
1437                        dataId =  this.skipToIdIndex;
1438                    }
1439                    var landmarkItem = {};
1440                    landmarkItem.dataId = dataId.toString();
1441                    landmarkItem.class = 'landmark';
1442                    landmarkItem.hasName = name.length > 0;
1443                    landmarkItem.name = this.getLocalizedLandmarkName(tagName, name);
1444                    landmarkItem.tagName = tagName;
1445                    landmarkItem.nestingLevel = 0;
1446                    if (allFlag) {
1447                        landmarkItem.nestingLevel = this.getNestingLevel(landmark, landmarks);
1448                    }
1449                    this.skipToIdIndex += 1;
1450                    allLandmarks.push(landmarkItem);
1451
1452                    // For sorting landmarks into groups
1453                    switch (tagName) {
1454                        case 'main':
1455                            mainElements.push(landmarkItem);
1456                            break;
1457                        case 'search':
1458                            searchElements.push(landmarkItem);
1459                            break;
1460                        case 'nav':
1461                            navElements.push(landmarkItem);
1462                            break;
1463                        case 'aside':
1464                            asideElements.push(landmarkItem);
1465                            break;
1466                        case 'footer':
1467                            footerElements.push(landmarkItem);
1468                            break;
1469                        case 'section':
1470                            // Regions must have accessible name to be included
1471                            if (landmarkItem.hasName) {
1472                                regionElements.push(landmarkItem);
1473                            }
1474                            break;
1475                        default:
1476                            otherElements.push(landmarkItem);
1477                            break;
1478                    }
1479                }
1480            }
1481            if (allFlag) {
1482                return allLandmarks;
1483            }
1484            return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, otherElements);
1485        }
1486    };
1487    // Initialize skipto menu button with onload event
1488    window.addEventListener('load', function() {
1489        SkipTo.init(window.SkipToConfig ||
1490            ((typeof window.Joomla === 'object' && typeof window.Joomla.getOptions === 'function') ? window.Joomla.getOptions('skipto-settings', {}) : {})
1491        );
1492    });
1493})();
1494/*@end @*/

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.