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 !== " "; 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.