[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 /*! skipto - v4.1.6 - 2022-03-09 2 * https://github.com/paypal/skipto 3 * Copyright (c) 2022 PayPal Accessibility Team and University of Illinois; Licensed BSD */ 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: '0', // default is the number zero 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}.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;background-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 this.buttonNode.setAttribute('accesskey', this.config.accesskey); 219 220 this.domNode.appendChild(this.buttonNode); 221 222 this.renderTooltip(this.domNode, this.buttonNode); 223 224 this.menuNode = document.createElement('div'); 225 this.menuNode.setAttribute('role', 'menu'); 226 this.menuNode.setAttribute('aria-busy', 'true'); 227 this.menuNode.setAttribute('id', this.skipToMenuId); 228 this.domNode.appendChild(this.menuNode); 229 this.buttonNode.addEventListener('keydown', this.handleButtonKeydown.bind(this)); 230 this.buttonNode.addEventListener('click', this.handleButtonClick.bind(this)); 231 this.buttonNode.addEventListener('focus', this.handleButtonFocus.bind(this)); 232 this.buttonNode.addEventListener('blur', this.handleButtonBlur.bind(this)); 233 this.buttonNode.addEventListener('pointerenter', this.handleButtonPointerenter.bind(this)); 234 this.buttonNode.addEventListener('pointerout', this.handleButtonPointerout.bind(this)); 235 this.domNode.addEventListener('focusin', this.handleFocusin.bind(this)); 236 this.domNode.addEventListener('focusout', this.handleFocusout.bind(this)); 237 window.addEventListener('pointerdown', this.handleBackgroundPointerdown.bind(this), true); 238 239 }, 240 renderTooltip: function(attachNode, buttonNode) { 241 var id = 'id-skip-to-tooltip'; 242 var accesskey = this.getBrowserSpecificAccesskey(this.config.accesskey); 243 244 var tooltip = this.config.buttonTooltip; 245 // for backward compatibility, support 'this.config.buttonTitle' if defined 246 if (this.isNotEmptyString(this.config.buttonTitle)) { 247 tooltip = this.config.buttonTitle; 248 } 249 250 this.tooltipLeft = buttonNode.getBoundingClientRect().width; 251 this.tooltipTop = buttonNode.getBoundingClientRect().height; 252 253 this.tooltipNode = document.createElement('div'); 254 this.tooltipNode.setAttribute('role', 'tooltip'); 255 this.tooltipNode.id = id; 256 this.tooltipNode.classList.add('skip-to-tooltip'); 257 258 if (this.isNotEmptyString(accesskey)) { 259 tooltip = this.config.buttonTooltipAccesskey.replace('$key', accesskey); 260 // for backward compatibility support 'buttonTitleWithAccesskey' if defined 261 if (this.isNotEmptyString(this.config.buttonTitleWithAccesskey)) { 262 tooltip = this.config.buttonTitleWithAccesskey.replace('$key', accesskey); 263 } 264 } 265 266 if (this.isEmptyString(tooltip)) { 267 // if there is no tooltip information 268 // do not display tooltip 269 this.config.enableTooltip = false; 270 } else { 271 this.tooltipNode.textContent = tooltip; 272 } 273 274 attachNode.appendChild(this.tooltipNode); 275 this.tooltipNode.style.left = this.tooltipLeft + 'px'; 276 this.tooltipNode.style.top = this.tooltipTop + 'px'; 277 278 // Temporarily show the tooltip to get rendered height 279 this.tooltipNode.classList.add('skip-to-show-tooltip'); 280 this.tooltipHeight = this.tooltipNode.getBoundingClientRect().height; 281 this.tooltipNode.classList.remove('skip-to-show-tooltip'); 282 }, 283 284 updateStyle: function(stylePlaceholder, value, defaultValue) { 285 if (typeof value !== 'string' || value.length === 0) { 286 value = defaultValue; 287 } 288 var index1 = this.defaultCSS.indexOf(stylePlaceholder); 289 var index2 = index1 + stylePlaceholder.length; 290 while (index1 >= 0 && index2 < this.defaultCSS.length) { 291 this.defaultCSS = this.defaultCSS.substring(0, index1) + value + this.defaultCSS.substring(index2); 292 index1 = this.defaultCSS.indexOf(stylePlaceholder, index2); 293 index2 = index1 + stylePlaceholder.length; 294 } 295 }, 296 addCSSColors: function() { 297 var theme = this.colorThemes['default']; 298 if (typeof this.colorThemes[this.config.colorTheme] === 'object') { 299 theme = this.colorThemes[this.config.colorTheme]; 300 } 301 this.updateStyle('$fontFamily', this.config.fontFamily, theme.fontFamily); 302 this.updateStyle('$fontSize', this.config.fontSize, theme.fontSize); 303 304 this.updateStyle('$positionLeft', this.config.positionLeft, theme.positionLeft); 305 306 this.updateStyle('$menuTextColor', this.config.menuTextColor, theme.menuTextColor); 307 this.updateStyle('$menuBackgroundColor', this.config.menuBackgroundColor, theme.menuBackgroundColor); 308 309 this.updateStyle('$menuitemFocusTextColor', this.config.menuitemFocusTextColor, theme.menuitemFocusTextColor); 310 this.updateStyle('$menuitemFocusBackgroundColor', this.config.menuitemFocusBackgroundColor, theme.menuitemFocusBackgroundColor); 311 312 this.updateStyle('$focusBorderColor', this.config.focusBorderColor, theme.focusBorderColor); 313 314 this.updateStyle('$buttonTextColor', this.config.buttonTextColor, theme.buttonTextColor); 315 this.updateStyle('$buttonBackgroundColor', this.config.buttonBackgroundColor, theme.buttonBackgroundColor); 316 }, 317 318 getBrowserSpecificAccesskey: function (accesskey) { 319 var userAgent = navigator.userAgent.toLowerCase(); 320 var platform = navigator.platform.toLowerCase(); 321 322 var hasWin = platform.indexOf('win') >= 0; 323 var hasMac = platform.indexOf('mac') >= 0; 324 var hasLinux = platform.indexOf('linux') >= 0 || platform.indexOf('bsd') >= 0; 325 326 var hasAndroid = userAgent.indexOf('android') >= 0; 327 var hasFirefox = userAgent.indexOf('firefox') >= 0; 328 var hasChrome = userAgent.indexOf('chrome') >= 0; 329 var hasOpera = userAgent.indexOf('opr') >= 0; 330 331 if (typeof accesskey !== 'string' || accesskey.length === 0) { 332 return ''; 333 } 334 335 if (hasWin || (hasLinux && !hasAndroid)) { 336 if (hasFirefox) { 337 return "Shift + Alt + " + accesskey; 338 } else { 339 if (hasChrome || hasOpera) { 340 return "Alt + " + accesskey; 341 } 342 } 343 } 344 345 if (hasMac) { 346 return "Ctrl + Option + " + accesskey; 347 } 348 349 return ''; 350 }, 351 setUpConfig: function(appConfig) { 352 var localConfig = this.config, 353 name, 354 appConfigSettings = typeof appConfig.settings !== 'undefined' ? appConfig.settings.skipTo : {}; 355 for (name in appConfigSettings) { 356 //overwrite values of our local config, based on the external config 357 if ((typeof localConfig[name] !== 'undefined') && 358 ((typeof appConfigSettings[name] === 'string') && 359 (appConfigSettings[name].length > 0 ) || 360 typeof appConfigSettings[name] === 'boolean') 361 ) { 362 localConfig[name] = appConfigSettings[name]; 363 } else { 364 throw new Error('** SkipTo Problem with user configuration option "' + name + '".'); 365 } 366 } 367 }, 368 renderStyleElement: function(cssString) { 369 var styleNode = document.createElement('style'); 370 var headNode = document.getElementsByTagName('head')[0]; 371 var css = document.createTextNode(cssString); 372 373 styleNode.setAttribute("type", "text/css"); 374 // ID is used to test whether skipto is already loaded 375 styleNode.id = this.skipToId; 376 styleNode.appendChild(css); 377 headNode.appendChild(styleNode); 378 }, 379 380 // 381 // Functions related to creating and populating the 382 // the popup menu 383 // 384 385 getFirstChar: function(menuitem) { 386 var c = ''; 387 var label = menuitem.querySelector('.label'); 388 if (label && this.isNotEmptyString(label.textContent)) { 389 c = label.textContent.trim()[0].toLowerCase(); 390 } 391 return c; 392 }, 393 394 getHeadingLevelFromAttribute: function(menuitem) { 395 var level = ''; 396 if (menuitem.hasAttribute('data-level')) { 397 level = menuitem.getAttribute('data-level'); 398 } 399 return level; 400 }, 401 402 updateKeyboardShortCuts: function () { 403 var mi; 404 this.firstChars = []; 405 this.headingLevels = []; 406 407 for(var i = 0; i < this.menuitemNodes.length; i += 1) { 408 mi = this.menuitemNodes[i]; 409 this.firstChars.push(this.getFirstChar(mi)); 410 this.headingLevels.push(this.getHeadingLevelFromAttribute(mi)); 411 } 412 }, 413 414 updateMenuitems: function () { 415 var menuitemNodes = this.menuNode.querySelectorAll('[role=menuitem'); 416 417 this.menuitemNodes = []; 418 for(var i = 0; i < menuitemNodes.length; i += 1) { 419 this.menuitemNodes.push(menuitemNodes[i]); 420 } 421 422 this.firstMenuitem = this.menuitemNodes[0]; 423 this.lastMenuitem = this.menuitemNodes[this.menuitemNodes.length-1]; 424 this.lastMenuitem.classList.add('last'); 425 this.updateKeyboardShortCuts(); 426 }, 427 428 renderMenuitemToGroup: function (groupNode, mi) { 429 var tagNode, tagNodeChild, labelNode, nestingNode; 430 431 var menuitemNode = document.createElement('div'); 432 menuitemNode.setAttribute('role', 'menuitem'); 433 menuitemNode.classList.add(mi.class); 434 if (this.isNotEmptyString(mi.tagName)) { 435 menuitemNode.classList.add('skip-to-' + mi.tagName.toLowerCase()); 436 } 437 menuitemNode.setAttribute('data-id', mi.dataId); 438 menuitemNode.tabIndex = -1; 439 if (this.isNotEmptyString(mi.ariaLabel)) { 440 menuitemNode.setAttribute('aria-label', mi.ariaLabel); 441 } 442 443 // add event handlers 444 menuitemNode.addEventListener('keydown', this.handleMenuitemKeydown.bind(this)); 445 menuitemNode.addEventListener('click', this.handleMenuitemClick.bind(this)); 446 menuitemNode.addEventListener('pointerenter', this.handleMenuitemPointerenter.bind(this)); 447 448 groupNode.appendChild(menuitemNode); 449 450 // add heading level and label 451 if (mi.class.includes('heading')) { 452 if (this.config.enableHeadingLevelShortcuts) { 453 tagNode = document.createElement('span'); 454 tagNodeChild = document.createElement('span'); 455 tagNodeChild.appendChild(document.createTextNode(mi.level)); 456 tagNode.append(tagNodeChild); 457 tagNode.appendChild(document.createTextNode(')')); 458 tagNode.classList.add('level'); 459 menuitemNode.append(tagNode); 460 } else { 461 menuitemNode.classList.add('no-level'); 462 } 463 menuitemNode.setAttribute('data-level', mi.level); 464 if (this.isNotEmptyString(mi.tagName)) { 465 menuitemNode.classList.add('skip-to-' + mi.tagName); 466 } 467 } 468 469 // add nesting level for landmarks 470 if (mi.class.includes('landmark')) { 471 menuitemNode.setAttribute('data-nesting', mi.nestingLevel); 472 menuitemNode.classList.add('skip-to-nesting-level-' + mi.nestingLevel); 473 474 if (mi.nestingLevel > 0 && (mi.nestingLevel > this.lastNestingLevel)) { 475 nestingNode = document.createElement('span'); 476 nestingNode.classList.add('nesting'); 477 menuitemNode.append(nestingNode); 478 } 479 this.lastNestingLevel = mi.nestingLevel; 480 } 481 482 labelNode = document.createElement('span'); 483 labelNode.appendChild(document.createTextNode(mi.name)); 484 labelNode.classList.add('label'); 485 menuitemNode.append(labelNode); 486 487 return menuitemNode; 488 }, 489 490 renderGroupLabel: function (groupLabelId, title, m, n) { 491 var titleNode, mofnNode, s; 492 var groupLabelNode = document.getElementById(groupLabelId); 493 494 titleNode = groupLabelNode.querySelector('.title'); 495 mofnNode = groupLabelNode.querySelector('.mofn'); 496 497 titleNode.textContent = title; 498 499 if (this.config.enableActions && this.config.enableMofN) { 500 if ((typeof m === 'number') && (typeof n === 'number')) { 501 s = this.config.mofnGroupLabel; 502 s = s.replace('$m', m); 503 s = s.replace('$n', n); 504 mofnNode.textContent = s; 505 } 506 } 507 }, 508 509 renderMenuitemGroup: function(groupId, title) { 510 var labelNode, groupNode, spanNode; 511 var menuNode = this.menuNode; 512 if (this.isNotEmptyString(title)) { 513 labelNode = document.createElement('div'); 514 labelNode.id = groupId + "-label"; 515 labelNode.setAttribute('role', 'separator'); 516 menuNode.appendChild(labelNode); 517 518 spanNode = document.createElement('span'); 519 spanNode.classList.add('title'); 520 spanNode.textContent = title; 521 labelNode.append(spanNode); 522 523 spanNode = document.createElement('span'); 524 spanNode.classList.add('mofn'); 525 labelNode.append(spanNode); 526 527 groupNode = document.createElement('div'); 528 groupNode.setAttribute('role', 'group'); 529 groupNode.setAttribute('aria-labelledby', labelNode.id); 530 groupNode.id = groupId; 531 menuNode.appendChild(groupNode); 532 menuNode = groupNode; 533 } 534 return groupNode; 535 }, 536 537 removeMenuitemGroup: function(groupId) { 538 var node = document.getElementById(groupId); 539 this.menuNode.removeChild(node); 540 node = document.getElementById(groupId + "-label"); 541 this.menuNode.removeChild(node); 542 }, 543 544 renderMenuitemsToGroup: function(groupNode, menuitems, msgNoItemsFound) { 545 groupNode.innerHTML = ''; 546 this.lastNestingLevel = 0; 547 548 if (menuitems.length === 0) { 549 var item = {}; 550 item.name = msgNoItemsFound; 551 item.tagName = ''; 552 item.class = 'no-items'; 553 item.dataId = ''; 554 this.renderMenuitemToGroup(groupNode, item); 555 } 556 else { 557 for (var i = 0; i < menuitems.length; i += 1) { 558 this.renderMenuitemToGroup(groupNode, menuitems[i]); 559 } 560 } 561 }, 562 563 getShowMoreHeadingsSelector: function(option) { 564 if (option === 'all') { 565 return this.showAllHeadingsSelector; 566 } 567 return this.config.headings; 568 }, 569 570 getShowMoreHeadingsLabel: function(option, n) { 571 var label = this.config.actionShowSelectedHeadingsLabel; 572 if (option === 'all') { 573 label = this.config.actionShowAllHeadingsLabel; 574 } 575 return label.replace('$num', n); 576 }, 577 578 getShowMoreHeadingsAriaLabel: function(option, n) { 579 var label = this.config.actionShowSelectedHeadingsAriaLabel; 580 581 if (option === 'all') { 582 label = this.config.actionShowAllHeadingsAriaLabel; 583 } 584 585 return label.replace('$num', n); 586 }, 587 588 renderActionMoreHeadings: function(groupNode) { 589 var item, menuitemNode; 590 var option = 'all'; 591 592 var selectedHeadingsLen = this.getHeadings(this.getShowMoreHeadingsSelector('selected')).length; 593 var allHeadingsLen = this.getHeadings(this.getShowMoreHeadingsSelector('all')).length; 594 var noAction = selectedHeadingsLen === allHeadingsLen; 595 var headingsLen = allHeadingsLen; 596 597 if (option !== 'all') { 598 headingsLen = selectedHeadingsLen; 599 } 600 601 if (!noAction) { 602 item = {}; 603 item.tagName = ''; 604 item.role = 'menuitem'; 605 item.class = 'action'; 606 item.dataId = 'skip-to-more-headings'; 607 item.name = this.getShowMoreHeadingsLabel(option, headingsLen); 608 item.ariaLabel = this.getShowMoreHeadingsAriaLabel(option, headingsLen); 609 610 menuitemNode = this.renderMenuitemToGroup(groupNode, item); 611 menuitemNode.setAttribute('data-show-heading-option', option); 612 menuitemNode.title = this.config.actionShowHeadingsHelp; 613 } 614 return noAction; 615 }, 616 617 updateHeadingGroupMenuitems: function(option) { 618 var headings, headingsLen, labelNode, groupNode; 619 620 var selectedHeadings = this.getHeadings(this.getShowMoreHeadingsSelector('selected')); 621 var selectedHeadingsLen = selectedHeadings.length; 622 var allHeadings = this.getHeadings(this.getShowMoreHeadingsSelector('all')); 623 var allHeadingsLen = allHeadings.length; 624 625 // Update list of headings 626 if ( option === 'all' ) { 627 headings = allHeadings; 628 } 629 else { 630 headings = selectedHeadings; 631 } 632 633 this.renderGroupLabel('id-skip-to-group-headings-label', this.config.headingGroupLabel, headings.length, allHeadings.length); 634 635 groupNode = document.getElementById('id-skip-to-group-headings'); 636 this.renderMenuitemsToGroup(groupNode, headings, this.config.msgNoHeadingsFound); 637 this.updateMenuitems(); 638 639 // Move focus to first heading menuitem 640 if (groupNode.firstElementChild) { 641 groupNode.firstElementChild.focus(); 642 } 643 644 // Update heading action menuitem 645 if (option === 'all') { 646 option = 'selected'; 647 headingsLen = selectedHeadingsLen; 648 } else { 649 option = 'all'; 650 headingsLen = allHeadingsLen; 651 } 652 653 var menuitemNode = this.menuNode.querySelector('[data-id=skip-to-more-headings]'); 654 menuitemNode.setAttribute('data-show-heading-option', option); 655 menuitemNode.setAttribute('aria-label', this.getShowMoreHeadingsAriaLabel(option, headingsLen)); 656 657 labelNode = menuitemNode.querySelector('span.label'); 658 labelNode.textContent = this.getShowMoreHeadingsLabel(option, headingsLen); 659 }, 660 661 getShowMoreLandmarksSelector: function(option) { 662 if (option === 'all') { 663 return this.showAllLandmarksSelector; 664 } 665 return this.config.landmarks; 666 }, 667 668 getShowMoreLandmarksLabel: function(option, n) { 669 var label = this.config.actionShowSelectedLandmarksLabel; 670 671 if (option === 'all') { 672 label = this.config.actionShowAllLandmarksLabel; 673 } 674 return label.replace('$num', n); 675 }, 676 677 getShowMoreLandmarksAriaLabel: function(option, n) { 678 var label = this.config.actionShowSelectedLandmarksAriaLabel; 679 680 if (option === 'all') { 681 label = this.config.actionShowAllLandmarksAriaLabel; 682 } 683 684 return label.replace('$num', n); 685 }, 686 687 renderActionMoreLandmarks: function(groupNode) { 688 var item, menuitemNode; 689 var option = 'all'; 690 691 var selectedLandmarksLen = this.getLandmarks(this.getShowMoreLandmarksSelector('selected')).length; 692 var allLandmarksLen = this.getLandmarks(this.getShowMoreLandmarksSelector('all')).length; 693 var noAction = selectedLandmarksLen === allLandmarksLen; 694 var landmarksLen = allLandmarksLen; 695 696 if (option !== 'all') { 697 landmarksLen = selectedLandmarksLen; 698 } 699 700 if (!noAction) { 701 item = {}; 702 item.tagName = ''; 703 item.role = 'menuitem'; 704 item.class = 'action'; 705 item.dataId = 'skip-to-more-landmarks'; 706 item.name = this.getShowMoreLandmarksLabel(option, landmarksLen); 707 item.ariaLabel = this.getShowMoreLandmarksAriaLabel(option, landmarksLen); 708 709 menuitemNode = this.renderMenuitemToGroup(groupNode, item); 710 711 menuitemNode.setAttribute('data-show-landmark-option', option); 712 menuitemNode.title = this.config.actionShowLandmarksHelp; 713 } 714 return noAction; 715 }, 716 717 updateLandmarksGroupMenuitems: function(option) { 718 var landmarks, landmarksLen, labelNode, groupNode; 719 var selectedLandmarks = this.getLandmarks(this.getShowMoreLandmarksSelector('selected')); 720 var selectedLandmarksLen = selectedLandmarks.length; 721 var allLandmarks = this.getLandmarks(this.getShowMoreLandmarksSelector('all'), true); 722 var allLandmarksLen = allLandmarks.length; 723 724 // Update landmark menu items 725 if ( option === 'all' ) { 726 landmarks = allLandmarks; 727 } 728 else { 729 landmarks = selectedLandmarks; 730 } 731 732 this.renderGroupLabel('id-skip-to-group-landmarks-label', this.config.landmarkGroupLabel, landmarks.length, allLandmarks.length); 733 734 groupNode = document.getElementById('id-skip-to-group-landmarks'); 735 this.renderMenuitemsToGroup(groupNode, landmarks, this.config.msgNoLandmarksFound); 736 this.updateMenuitems(); 737 738 // Move focus to first landmark menuitem 739 if (groupNode.firstElementChild) { 740 groupNode.firstElementChild.focus(); 741 } 742 743 // Update landmark action menuitem 744 if (option === 'all') { 745 option = 'selected'; 746 landmarksLen = selectedLandmarksLen; 747 } else { 748 option = 'all'; 749 landmarksLen = allLandmarksLen; 750 } 751 752 var menuitemNode = this.menuNode.querySelector('[data-id=skip-to-more-landmarks]'); 753 menuitemNode.setAttribute('data-show-landmark-option', option); 754 menuitemNode.setAttribute('aria-label', this.getShowMoreLandmarksAriaLabel(option, landmarksLen)); 755 756 labelNode = menuitemNode.querySelector('span.label'); 757 labelNode.textContent = this.getShowMoreLandmarksLabel(option, landmarksLen); 758 }, 759 760 renderMenu: function() { 761 var groupNode, 762 selectedLandmarks, 763 allLandmarks, 764 landmarkElements, 765 selectedHeadings, 766 allHeadings, 767 headingElements, 768 selector, 769 option, 770 hasNoAction1, 771 hasNoAction2; 772 // remove current menu items from menu 773 while (this.menuNode.lastElementChild) { 774 this.menuNode.removeChild(this.menuNode.lastElementChild); 775 } 776 777 option = 'selected'; 778 // Create landmarks group 779 selector = this.getShowMoreLandmarksSelector('all'); 780 allLandmarks = this.getLandmarks(selector, true); 781 selector = this.getShowMoreLandmarksSelector('selected'); 782 selectedLandmarks = this.getLandmarks(selector); 783 landmarkElements = selectedLandmarks; 784 785 if (option === 'all') { 786 landmarkElements = allLandmarks; 787 } 788 789 groupNode = this.renderMenuitemGroup('id-skip-to-group-landmarks', this.config.landmarkGroupLabel); 790 this.renderMenuitemsToGroup(groupNode, landmarkElements, this.config.msgNoLandmarksFound); 791 this.renderGroupLabel('id-skip-to-group-landmarks-label', this.config.landmarkGroupLabel, landmarkElements.length, allLandmarks.length); 792 793 // Create headings group 794 selector = this.getShowMoreHeadingsSelector('all'); 795 allHeadings = this.getHeadings(selector); 796 selector = this.getShowMoreHeadingsSelector('selected'); 797 selectedHeadings = this.getHeadings(selector); 798 headingElements = selectedHeadings; 799 800 if (option === 'all') { 801 headingElements = allHeadings; 802 } 803 804 groupNode = this.renderMenuitemGroup('id-skip-to-group-headings', this.config.headingGroupLabel); 805 this.renderMenuitemsToGroup(groupNode, headingElements, this.config.msgNoHeadingsFound); 806 this.renderGroupLabel('id-skip-to-group-headings-label', this.config.headingGroupLabel, headingElements.length, allHeadings.length); 807 808 // Create actions, if enabled 809 if (this.config.enableActions) { 810 groupNode = this.renderMenuitemGroup('id-skip-to-group-actions', this.config.actionGroupLabel); 811 hasNoAction1 = this.renderActionMoreLandmarks(groupNode); 812 hasNoAction2 = this.renderActionMoreHeadings(groupNode); 813 // Remove action label if no actions are available 814 if (hasNoAction1 && hasNoAction2) { 815 this.removeMenuitemGroup('id-skip-to-group-actions'); 816 } 817 } 818 819 // Update list of menuitems 820 this.updateMenuitems(); 821 }, 822 823 // 824 // Menu scripting event functions and utilities 825 // 826 827 setFocusToMenuitem: function(menuitem) { 828 if (menuitem) { 829 menuitem.focus(); 830 } 831 }, 832 833 setFocusToFirstMenuitem: function() { 834 this.setFocusToMenuitem(this.firstMenuitem); 835 }, 836 837 setFocusToLastMenuitem: function() { 838 this.setFocusToMenuitem(this.lastMenuitem); 839 }, 840 841 setFocusToPreviousMenuitem: function(menuitem) { 842 var newMenuitem, index; 843 if (menuitem === this.firstMenuitem) { 844 newMenuitem = this.lastMenuitem; 845 } else { 846 index = this.menuitemNodes.indexOf(menuitem); 847 newMenuitem = this.menuitemNodes[index - 1]; 848 } 849 this.setFocusToMenuitem(newMenuitem); 850 return newMenuitem; 851 }, 852 853 setFocusToNextMenuitem: function(menuitem) { 854 var newMenuitem, index; 855 if (menuitem === this.lastMenuitem) { 856 newMenuitem = this.firstMenuitem; 857 } else { 858 index = this.menuitemNodes.indexOf(menuitem); 859 newMenuitem = this.menuitemNodes[index + 1]; 860 } 861 this.setFocusToMenuitem(newMenuitem); 862 return newMenuitem; 863 }, 864 865 setFocusByFirstCharacter: function(menuitem, char) { 866 var start, index; 867 if (char.length > 1) { 868 return; 869 } 870 char = char.toLowerCase(); 871 872 // Get start index for search based on position of currentItem 873 start = this.menuitemNodes.indexOf(menuitem) + 1; 874 if (start >= this.menuitemNodes.length) { 875 start = 0; 876 } 877 878 // Check remaining items in the menu 879 index = this.firstChars.indexOf(char, start); 880 881 // If not found in remaining items, check headings 882 if (index === -1) { 883 index = this.headingLevels.indexOf(char, start); 884 } 885 886 // If not found in remaining items, check from beginning 887 if (index === -1) { 888 index = this.firstChars.indexOf(char, 0); 889 } 890 891 // If not found in remaining items, check headings from beginning 892 if (index === -1) { 893 index = this.headingLevels.indexOf(char, 0); 894 } 895 896 // If match was found... 897 if (index > -1) { 898 this.setFocusToMenuitem(this.menuitemNodes[index]); 899 } 900 }, 901 902 // Utilities 903 getIndexFirstChars: function(startIndex, char) { 904 for (var i = startIndex; i < this.firstChars.length; i += 1) { 905 if (char === this.firstChars[i]) { 906 return i; 907 } 908 } 909 return -1; 910 }, 911 // Popup menu methods 912 openPopup: function() { 913 this.menuNode.setAttribute('aria-busy', 'true'); 914 this.renderMenu(); 915 this.menuNode.style.display = 'block'; 916 this.menuNode.removeAttribute('aria-busy'); 917 this.buttonNode.setAttribute('aria-expanded', 'true'); 918 }, 919 920 closePopup: function() { 921 if (this.isOpen()) { 922 this.buttonNode.setAttribute('aria-expanded', 'false'); 923 this.menuNode.style.display = 'none'; 924 } 925 }, 926 isOpen: function() { 927 return this.buttonNode.getAttribute('aria-expanded') === 'true'; 928 }, 929 // Menu event handlers 930 handleFocusin: function() { 931 this.domNode.classList.add('focus'); 932 }, 933 handleFocusout: function() { 934 this.domNode.classList.remove('focus'); 935 }, 936 handleButtonKeydown: function(event) { 937 var key = event.key, 938 flag = false; 939 switch (key) { 940 case ' ': 941 case 'Enter': 942 case 'ArrowDown': 943 case 'Down': 944 this.openPopup(); 945 this.setFocusToFirstMenuitem(); 946 flag = true; 947 break; 948 case 'Esc': 949 case 'Escape': 950 this.closePopup(); 951 this.buttonNode.focus(); 952 this.hideTooltip(); 953 flag = true; 954 break; 955 case 'Up': 956 case 'ArrowUp': 957 this.openPopup(); 958 this.setFocusToLastMenuitem(); 959 flag = true; 960 break; 961 default: 962 break; 963 } 964 if (flag) { 965 event.stopPropagation(); 966 event.preventDefault(); 967 } 968 }, 969 handleButtonClick: function(event) { 970 if (this.isOpen()) { 971 this.closePopup(); 972 this.buttonNode.focus(); 973 } else { 974 this.openPopup(); 975 this.setFocusToFirstMenuitem(); 976 } 977 event.stopPropagation(); 978 event.preventDefault(); 979 }, 980 isTooltipHidden: function() { 981 return this.tooltipNode.className.indexOf('skip-to-show-tooltip') < 0; 982 }, 983 displayTooltip: function() { 984 if (this.showTooltipFocus || this.showTooltipHover) { 985 this.tooltipNode.classList.add('skip-to-show-tooltip'); 986 } 987 }, 988 showTooltip: function() { 989 this.showTooltipFocus = true; 990 if (this.config.enableTooltip && this.isTooltipHidden()) { 991 this.tooltipNode.style.left = this.tooltipLeft + 'px'; 992 this.tooltipNode.style.top = this.tooltipTop + 'px'; 993 setTimeout(this.displayTooltip.bind(this), this.tooltipTimerDelay); 994 } 995 }, 996 hideTooltip: function() { 997 this.showTooltipFocus = false; 998 if(this.config.enableTooltip) { 999 this.tooltipNode.classList.remove('skip-to-show-tooltip'); 1000 } 1001 }, 1002 handleButtonFocus: function() { 1003 this.showTooltip(); 1004 }, 1005 handleButtonBlur: function() { 1006 this.hideTooltip(); 1007 }, 1008 handleButtonPointerenter: function(event) { 1009 this.showTooltipHover = true; 1010 if (this.config.enableTooltip && this.isTooltipHidden()) { 1011 var rect = this.buttonNode.getBoundingClientRect(); 1012 var left = Math.min(this.tooltipLeft, event.pageX - rect.left + this.tooltipHeight); 1013 this.tooltipNode.style.left = left + 'px'; 1014 var top = event.pageY - rect.top; 1015 this.tooltipNode.style.top = top + 'px'; 1016 setTimeout(this.showTooltip. bind(this), this.tooltipTimerDelay); 1017 } 1018 }, 1019 handleButtonPointerout: function() { 1020 this.showTooltipHover = false; 1021 if(this.config.enableTooltip) { 1022 this.tooltipNode.classList.remove('skip-to-show-tooltip'); 1023 } 1024 }, 1025 skipToElement: function(menuitem) { 1026 1027 var isVisible = this.isVisible; 1028 var focusNode = false; 1029 var scrollNode = false; 1030 var elem; 1031 1032 function findVisibleElement(e, selectors) { 1033 if (e) { 1034 for (var j = 0; j < selectors.length; j += 1) { 1035 var elems = e.querySelectorAll(selectors[j]); 1036 for(var i = 0; i < elems.length; i +=1) { 1037 if (isVisible(elems[i])) { 1038 return elems[i]; 1039 } 1040 } 1041 } 1042 } 1043 return e; 1044 } 1045 1046 var searchSelectors = ['input', 'button', 'input[type=button]', 'input[type=submit]', 'a']; 1047 var navigationSelectors = ['a', 'input', 'button', 'input[type=button]', 'input[type=submit]']; 1048 var landmarkSelectors = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'article', 'p', 'li', 'a']; 1049 1050 var isLandmark = menuitem.classList.contains('landmark'); 1051 var isSearch = menuitem.classList.contains('skip-to-search'); 1052 var isNav = menuitem.classList.contains('skip-to-nav'); 1053 1054 elem = document.querySelector('[data-skip-to-id="' + menuitem.getAttribute('data-id') + '"]'); 1055 1056 if (elem) { 1057 if (isSearch) { 1058 focusNode = findVisibleElement(elem, searchSelectors); 1059 } 1060 if (isNav) { 1061 focusNode = findVisibleElement(elem, navigationSelectors); 1062 } 1063 if (focusNode && this.isVisible(focusNode)) { 1064 focusNode.focus(); 1065 focusNode.scrollIntoView({block: 'nearest'}); 1066 } 1067 else { 1068 if (isLandmark) { 1069 scrollNode = findVisibleElement(elem, landmarkSelectors); 1070 if (scrollNode) { 1071 elem = scrollNode; 1072 } 1073 } 1074 elem.tabIndex = -1; 1075 elem.focus(); 1076 elem.scrollIntoView({block: 'center'}); 1077 } 1078 } 1079 }, 1080 handleMenuitemAction: function(tgt) { 1081 var option; 1082 switch (tgt.getAttribute('data-id')) { 1083 case '': 1084 // this means there were no headings or landmarks in the list 1085 break; 1086 1087 case 'skip-to-more-headings': 1088 option = tgt.getAttribute('data-show-heading-option'); 1089 this.updateHeadingGroupMenuitems(option); 1090 break; 1091 1092 case 'skip-to-more-landmarks': 1093 option = tgt.getAttribute('data-show-landmark-option'); 1094 this.updateLandmarksGroupMenuitems(option); 1095 break; 1096 1097 default: 1098 this.closePopup(); 1099 this.skipToElement(tgt); 1100 break; 1101 } 1102 }, 1103 handleMenuitemKeydown: function(event) { 1104 var tgt = event.currentTarget, 1105 key = event.key, 1106 flag = false; 1107 1108 function isPrintableCharacter(str) { 1109 return str.length === 1 && str.match(/\S/); 1110 } 1111 if (event.ctrlKey || event.altKey || event.metaKey) { 1112 return; 1113 } 1114 if (event.shiftKey) { 1115 if (isPrintableCharacter(key)) { 1116 this.setFocusByFirstCharacter(tgt, key); 1117 flag = true; 1118 } 1119 if (event.key === 'Tab') { 1120 this.buttonNode.focus(); 1121 this.closePopup(); 1122 flag = true; 1123 } 1124 } else { 1125 switch (key) { 1126 case 'Enter': 1127 case ' ': 1128 this.handleMenuitemAction(tgt); 1129 flag = true; 1130 break; 1131 case 'Esc': 1132 case 'Escape': 1133 this.closePopup(); 1134 this.buttonNode.focus(); 1135 flag = true; 1136 break; 1137 case 'Up': 1138 case 'ArrowUp': 1139 this.setFocusToPreviousMenuitem(tgt); 1140 flag = true; 1141 break; 1142 case 'ArrowDown': 1143 case 'Down': 1144 this.setFocusToNextMenuitem(tgt); 1145 flag = true; 1146 break; 1147 case 'Home': 1148 case 'PageUp': 1149 this.setFocusToFirstMenuitem(); 1150 flag = true; 1151 break; 1152 case 'End': 1153 case 'PageDown': 1154 this.setFocusToLastMenuitem(); 1155 flag = true; 1156 break; 1157 case 'Tab': 1158 this.closePopup(); 1159 break; 1160 default: 1161 if (isPrintableCharacter(key)) { 1162 this.setFocusByFirstCharacter(tgt, key); 1163 flag = true; 1164 } 1165 break; 1166 } 1167 } 1168 if (flag) { 1169 event.stopPropagation(); 1170 event.preventDefault(); 1171 } 1172 }, 1173 handleMenuitemClick: function(event) { 1174 this.handleMenuitemAction(event.currentTarget); 1175 event.stopPropagation(); 1176 event.preventDefault(); 1177 }, 1178 handleMenuitemPointerenter: function(event) { 1179 var tgt = event.currentTarget; 1180 tgt.focus(); 1181 }, 1182 handleBackgroundPointerdown: function(event) { 1183 if (!this.domNode.contains(event.target)) { 1184 if (this.isOpen()) { 1185 this.closePopup(); 1186 this.buttonNode.focus(); 1187 } 1188 } 1189 }, 1190 // methods to extract landmarks, headings and ids 1191 normalizeName: function(name) { 1192 if (typeof name === 'string') return name.replace(/\w\S*/g, function(txt) { 1193 return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 1194 }); 1195 return ""; 1196 }, 1197 getTextContent: function(elem) { 1198 function getText(e, strings) { 1199 // If text node get the text and return 1200 if (e.nodeType === Node.TEXT_NODE) { 1201 strings.push(e.data); 1202 } else { 1203 // if an element for through all the children elements looking for text 1204 if (e.nodeType === Node.ELEMENT_NODE) { 1205 // check to see if IMG or AREA element and to use ALT content if defined 1206 var tagName = e.tagName.toLowerCase(); 1207 if ((tagName === 'img') || (tagName === 'area')) { 1208 if (e.alt) { 1209 strings.push(e.alt); 1210 } 1211 } else { 1212 var c = e.firstChild; 1213 while (c) { 1214 getText(c, strings); 1215 c = c.nextSibling; 1216 } // end loop 1217 } 1218 } 1219 } 1220 } // end function getStrings 1221 // Create return object 1222 var str = "Test", 1223 strings = []; 1224 getText(elem, strings); 1225 if (strings.length) str = strings.join(" "); 1226 return str; 1227 }, 1228 getAccessibleName: function(elem) { 1229 var labelledbyIds = elem.getAttribute('aria-labelledby'), 1230 label = elem.getAttribute('aria-label'), 1231 title = elem.getAttribute('title'), 1232 name = ""; 1233 if (labelledbyIds && labelledbyIds.length) { 1234 var str, 1235 strings = [], 1236 ids = labelledbyIds.split(' '); 1237 if (!ids.length) ids = [labelledbyIds]; 1238 for (var i = 0, l = ids.length; i < l; i += 1) { 1239 var e = document.getElementById(ids[i]); 1240 if (e) str = this.getTextContent(e); 1241 if (str && str.length) strings.push(str); 1242 } 1243 name = strings.join(" "); 1244 } else { 1245 if (this.isNotEmptyString(label)) { 1246 name = label; 1247 } else { 1248 if (this.isNotEmptyString(title)) { 1249 name = title; 1250 } 1251 } 1252 } 1253 return name; 1254 }, 1255 isVisible: function(element) { 1256 function isVisibleRec(el) { 1257 if (el.nodeType === 9) return true; /*IE8 does not support Node.DOCUMENT_NODE*/ 1258 var computedStyle = window.getComputedStyle(el); 1259 var display = computedStyle.getPropertyValue('display'); 1260 var visibility = computedStyle.getPropertyValue('visibility'); 1261 var hidden = el.getAttribute('hidden'); 1262 if ((display === 'none') || 1263 (visibility === 'hidden') || 1264 (hidden !== null)) { 1265 return false; 1266 } 1267 return isVisibleRec(el.parentNode); 1268 } 1269 return isVisibleRec(element); 1270 }, 1271 getHeadings: function(targets) { 1272 var dataId, level; 1273 if (typeof targets !== 'string') { 1274 targets = this.config.headings; 1275 } 1276 var headingElementsArr = []; 1277 if (typeof targets !== 'string' || targets.length === 0) return; 1278 var headings = document.querySelectorAll(targets); 1279 for (var i = 0, len = headings.length; i < len; i += 1) { 1280 var heading = headings[i]; 1281 var role = heading.getAttribute('role'); 1282 if ((typeof role === 'string') && (role === 'presentation')) continue; 1283 if (this.isVisible(heading) && this.isNotEmptyString(heading.innerHTML)) { 1284 if (heading.hasAttribute('data-skip-to-id')) { 1285 dataId = heading.getAttribute('data-skip-to-id'); 1286 } else { 1287 heading.setAttribute('data-skip-to-id', this.skipToIdIndex); 1288 dataId = this.skipToIdIndex; 1289 } 1290 level = heading.tagName.substring(1); 1291 var headingItem = {}; 1292 headingItem.dataId = dataId.toString(); 1293 headingItem.class = 'heading'; 1294 headingItem.name = this.getTextContent(heading); 1295 headingItem.ariaLabel = headingItem.name + ', '; 1296 headingItem.ariaLabel += this.config.headingLevelLabel + ' ' + level; 1297 headingItem.tagName = heading.tagName.toLowerCase(); 1298 headingItem.role = 'heading'; 1299 headingItem.level = level; 1300 headingElementsArr.push(headingItem); 1301 this.skipToIdIndex += 1; 1302 } 1303 } 1304 return headingElementsArr; 1305 }, 1306 getLocalizedLandmarkName: function(tagName, name) { 1307 var n; 1308 switch (tagName) { 1309 case 'aside': 1310 n = this.config.asideLabel; 1311 break; 1312 case 'footer': 1313 n = this.config.footerLabel; 1314 break; 1315 case 'form': 1316 n = this.config.formLabel; 1317 break; 1318 case 'header': 1319 n = this.config.headerLabel; 1320 break; 1321 case 'main': 1322 n = this.config.mainLabel; 1323 break; 1324 case 'nav': 1325 n = this.config.navLabel; 1326 break; 1327 case 'section': 1328 case 'region': 1329 n = this.config.regionLabel; 1330 break; 1331 case 'search': 1332 n = this.config.searchLabel; 1333 break; 1334 // When an ID is used as a selector, assume for main content 1335 default: 1336 n = tagName; 1337 break; 1338 } 1339 if (this.isNotEmptyString(name)) { 1340 n += ': ' + name; 1341 } 1342 return n; 1343 }, 1344 getNestingLevel: function(landmark, landmarks) { 1345 var nestingLevel = 0; 1346 var parentNode = landmark.parentNode; 1347 while (parentNode) { 1348 for (var i = 0; i < landmarks.length; i += 1) { 1349 if (landmarks[i] === parentNode) { 1350 nestingLevel += 1; 1351 // no more than 3 levels of nesting supported 1352 if (nestingLevel === 3) { 1353 return 3; 1354 } 1355 continue; 1356 } 1357 } 1358 parentNode = parentNode.parentNode; 1359 } 1360 return nestingLevel; 1361 }, 1362 getLandmarks: function(targets, allFlag) { 1363 if (typeof allFlag !== 'boolean') { 1364 allFlag = false; 1365 } 1366 if (typeof targets !== 'string') { 1367 targets = this.config.landmarks; 1368 } 1369 var landmarks = document.querySelectorAll(targets); 1370 var mainElements = []; 1371 var searchElements = []; 1372 var navElements = []; 1373 var asideElements = []; 1374 var footerElements = []; 1375 var regionElements = []; 1376 var otherElements = []; 1377 var allLandmarks = []; 1378 var dataId = ''; 1379 for (var i = 0, len = landmarks.length; i < len; i += 1) { 1380 var landmark = landmarks[i]; 1381 // if skipto is a landmark don't include it in the list 1382 if (landmark === this.domNode) { 1383 continue; 1384 } 1385 var role = landmark.getAttribute('role'); 1386 var tagName = landmark.tagName.toLowerCase(); 1387 if ((typeof role === 'string') && (role === 'presentation')) continue; 1388 if (this.isVisible(landmark)) { 1389 if (!role) role = tagName; 1390 var name = this.getAccessibleName(landmark); 1391 if (typeof name !== 'string') { 1392 name = ''; 1393 } 1394 // normalize tagNames 1395 switch (role) { 1396 case 'banner': 1397 tagName = 'header'; 1398 break; 1399 case 'complementary': 1400 tagName = 'aside'; 1401 break; 1402 case 'contentinfo': 1403 tagName = 'footer'; 1404 break; 1405 case 'form': 1406 tagName = 'form'; 1407 break; 1408 case 'main': 1409 tagName = 'main'; 1410 break; 1411 case 'navigation': 1412 tagName = 'nav'; 1413 break; 1414 case 'region': 1415 tagName = 'section'; 1416 break; 1417 case 'search': 1418 tagName = 'search'; 1419 break; 1420 default: 1421 break; 1422 } 1423 // if using ID for selectQuery give tagName as main 1424 if (['aside', 'footer', 'form', 'header', 'main', 'nav', 'section', 'search'].indexOf(tagName) < 0) { 1425 tagName = 'main'; 1426 } 1427 if (landmark.hasAttribute('aria-roledescription')) { 1428 tagName = landmark.getAttribute('aria-roledescription').trim().replace(' ', '-'); 1429 } 1430 if (landmark.hasAttribute('data-skip-to-id')) { 1431 dataId = landmark.getAttribute('data-skip-to-id'); 1432 } else { 1433 landmark.setAttribute('data-skip-to-id', this.skipToIdIndex); 1434 dataId = this.skipToIdIndex; 1435 } 1436 var landmarkItem = {}; 1437 landmarkItem.dataId = dataId.toString(); 1438 landmarkItem.class = 'landmark'; 1439 landmarkItem.hasName = name.length > 0; 1440 landmarkItem.name = this.getLocalizedLandmarkName(tagName, name); 1441 landmarkItem.tagName = tagName; 1442 landmarkItem.nestingLevel = 0; 1443 if (allFlag) { 1444 landmarkItem.nestingLevel = this.getNestingLevel(landmark, landmarks); 1445 } 1446 this.skipToIdIndex += 1; 1447 allLandmarks.push(landmarkItem); 1448 1449 // For sorting landmarks into groups 1450 switch (tagName) { 1451 case 'main': 1452 mainElements.push(landmarkItem); 1453 break; 1454 case 'search': 1455 searchElements.push(landmarkItem); 1456 break; 1457 case 'nav': 1458 navElements.push(landmarkItem); 1459 break; 1460 case 'aside': 1461 asideElements.push(landmarkItem); 1462 break; 1463 case 'footer': 1464 footerElements.push(landmarkItem); 1465 break; 1466 case 'section': 1467 // Regions must have accessible name to be included 1468 if (landmarkItem.hasName) { 1469 regionElements.push(landmarkItem); 1470 } 1471 break; 1472 default: 1473 otherElements.push(landmarkItem); 1474 break; 1475 } 1476 } 1477 } 1478 if (allFlag) { 1479 return allLandmarks; 1480 } 1481 return [].concat(mainElements, searchElements, navElements, asideElements, regionElements, footerElements, otherElements); 1482 } 1483 }; 1484 // Initialize skipto menu button with onload event 1485 window.addEventListener('load', function() { 1486 SkipTo.init(window.SkipToConfig || 1487 ((typeof window.Joomla === 'object' && typeof window.Joomla.getOptions === 'function') ? window.Joomla.getOptions('skipto-settings', {}) : {}) 1488 ); 1489 }); 1490 })(); 1491 /*@end @*/
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |