[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/media/vendor/skipto/js/ -> skipto.js (source)

   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 !== "&nbsp;";
 154      },
 155      isEmptyString: function(str) {
 156        return (typeof str !== 'string') || str.length === 0 && !str.trim();
 157      },
 158      init: function(config) {
 159        var node;
 160        // Check if skipto is already loaded

 161  
 162        if (document.querySelector('style#' + this.skipToId)) {
 163          return;
 164        }
 165  
 166        var attachElement = document.body;
 167        if (config) {
 168          this.setUpConfig(config);
 169        }
 170        if (typeof this.config.attachElement === 'string') {
 171          node = document.querySelector(this.config.attachElement);
 172          if (node && node.nodeType === Node.ELEMENT_NODE) {
 173            attachElement = node;
 174          }
 175        }
 176        this.addCSSColors();
 177        this.renderStyleElement(this.defaultCSS);
 178        var elem = this.config.containerElement.toLowerCase().trim();
 179        if (!this.isNotEmptyString(elem)) {
 180          elem = 'div';
 181        }
 182        this.domNode = document.createElement(elem);
 183        this.domNode.classList.add('skip-to');
 184        if (this.isNotEmptyString(this.config.customClass)) {
 185          this.domNode.classList.add(this.config.customClass);
 186        }
 187        if (this.isNotEmptyString(this.config.containerRole)) {
 188          this.domNode.setAttribute('role', this.config.containerRole);
 189        }
 190        var displayOption = this.config.displayOption;
 191        if (typeof displayOption === 'string') {
 192          displayOption = displayOption.trim().toLowerCase();
 193          if (displayOption.length) {
 194            switch (this.config.displayOption) {
 195              case 'fixed':
 196                this.domNode.classList.add('fixed');
 197                break;
 198              case 'onfocus':  // Legacy option
 199              case 'popup':
 200                this.domNode.classList.add('popup');
 201                break;
 202              default:
 203                break;
 204            }
 205          }
 206        }
 207        // Place skip to at the beginning of the document

 208        if (attachElement.firstElementChild) {
 209          attachElement.insertBefore(this.domNode, attachElement.firstElementChild);
 210        } else {
 211          attachElement.appendChild(this.domNode);
 212        }
 213        this.buttonNode = document.createElement('button');
 214        this.buttonNode.textContent = this.config.buttonLabel;
 215        this.buttonNode.setAttribute('aria-haspopup', 'true');
 216        this.buttonNode.setAttribute('aria-expanded', 'false');
 217        this.buttonNode.setAttribute('aria-controls', this.skipToMenuId);
 218        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 @*/



Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer