Advertisement
Guest User

touch-keyboard-navigation.js

a guest
Jan 2nd, 2019
174
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2.  * Touch & Keyboard navigation.
  3.  *
  4.  * Contains handlers for touch devices and keyboard navigation.
  5.  */
  6.  
  7. (function() {
  8.  
  9.     /**
  10.      * Debounce
  11.      *
  12.      * @param {Function} func
  13.      * @param {number} wait
  14.      * @param {boolean} immediate
  15.      */
  16.     function debounce(func, wait, immediate) {
  17.         'use strict';
  18.  
  19.         var timeout;
  20.         wait      = (typeof wait !== 'undefined') ? wait : 20;
  21.         immediate = (typeof immediate !== 'undefined') ? immediate : true;
  22.  
  23.         return function() {
  24.  
  25.             var context = this, args = arguments;
  26.             var later = function() {
  27.                 timeout = null;
  28.  
  29.                 if (!immediate) {
  30.                     func.apply(context, args);
  31.                 }
  32.             };
  33.  
  34.             var callNow = immediate && !timeout;
  35.  
  36.             clearTimeout(timeout);
  37.             timeout = setTimeout(later, wait);
  38.  
  39.             if (callNow) {
  40.                 func.apply(context, args);
  41.             }
  42.         };
  43.     }
  44.  
  45.     /**
  46.      * Add class
  47.      *
  48.      * @param {Object} el
  49.      * @param {string} cls
  50.      */
  51.     function addClass(el, cls) {
  52.         if ( ! el.className.match( '(?:^|\\s)' + cls + '(?!\\S)') ) {
  53.             el.className += ' ' + cls;
  54.         }
  55.     }
  56.  
  57.     /**
  58.      * Delete class
  59.      *
  60.      * @param {Object} el
  61.      * @param {string} cls
  62.      */
  63.     function deleteClass(el, cls) {
  64.         el.className = el.className.replace( new RegExp( '(?:^|\\s)' + cls + '(?!\\S)' ),'' );
  65.     }
  66.  
  67.     /**
  68.      * Has class?
  69.      *
  70.      * @param {Object} el
  71.      * @param {string} cls
  72.      *
  73.      * @returns {boolean} Has class
  74.      */
  75.     function hasClass(el, cls) {
  76.  
  77.         if ( el.className.match( '(?:^|\\s)' + cls + '(?!\\S)' ) ) {
  78.             return true;
  79.         }
  80.     }
  81.  
  82.     /**
  83.      * Toggle Aria Expanded state for screenreaders
  84.      *
  85.      * @param {Object} ariaItem
  86.      */
  87.     function toggleAriaExpandedState( ariaItem ) {
  88.         'use strict';
  89.  
  90.         var ariaState = ariaItem.getAttribute('aria-expanded');
  91.  
  92.         if ( ariaState === 'true' ) {
  93.             ariaState = 'false';
  94.         } else {
  95.             ariaState = 'true';
  96.         }
  97.  
  98.         ariaItem.setAttribute('aria-expanded', ariaState);
  99.     }
  100.  
  101.     /**
  102.      * Open sub-menu
  103.      *
  104.      * @param {Object} currentSubMenu
  105.      */
  106.     function openSubMenu( currentSubMenu ) {
  107.         'use strict';
  108.  
  109.         // Update classes
  110.         // classList.add is not supported in IE11
  111.         currentSubMenu.parentElement.className += ' off-canvas';
  112.         currentSubMenu.parentElement.lastElementChild.className += ' expanded-true';
  113.  
  114.         // Update aria-expanded state
  115.         toggleAriaExpandedState( currentSubMenu.previousSibling );
  116.     }
  117.  
  118.     /**
  119.      * Close sub-menu
  120.      *
  121.      * @param {Object} currentSubMenu
  122.      */
  123.     function closeSubMenu( currentSubMenu ) {
  124.         'use strict';
  125.  
  126.         var menuItem     = getCurrentParent( currentSubMenu, '.menu-item' ); // this.parentNode
  127.         var menuItemAria = menuItem.querySelector('a[aria-expanded]');
  128.         var subMenu      = currentSubMenu.closest('.sub-menu');
  129.  
  130.         // If this is in a sub-sub-menu, go back to parent sub-menu
  131.         if ( getCurrentParent( currentSubMenu, 'ul' ).classList.contains( 'sub-menu' ) ) {
  132.  
  133.             // Update classes
  134.             // classList.remove is not supported in IE11
  135.             menuItem.className = menuItem.className.replace( 'off-canvas', '' );
  136.             subMenu.className  = subMenu.className.replace( 'expanded-true', '' );
  137.  
  138.             // Update aria-expanded and :focus states
  139.             toggleAriaExpandedState( menuItemAria );
  140.  
  141.         // Or else close all sub-menus
  142.         } else {
  143.  
  144.             // Update classes
  145.             // classList.remove is not supported in IE11
  146.             menuItem.className = menuItem.className.replace( 'off-canvas', '' );
  147.             menuItem.lastElementChild.className = menuItem.lastElementChild.className.replace( 'expanded-true', '' );
  148.  
  149.             // Update aria-expanded and :focus states
  150.             toggleAriaExpandedState( menuItemAria );
  151.         }
  152.     }
  153.  
  154.     /**
  155.      * Find first ancestor of an element by selector
  156.      *
  157.      * @param {Object} child
  158.      * @param {String} selector
  159.      * @param {String} stopSelector
  160.      */
  161.     function getCurrentParent( child, selector, stopSelector ) {
  162.  
  163.         var currentParent = null;
  164.  
  165.         while ( child ) {
  166.  
  167.             if ( child.matches(selector) ) {
  168.  
  169.                 currentParent = child;
  170.                 break;
  171.  
  172.             } else if ( stopSelector && child.matches(stopSelector) ) {
  173.  
  174.                 break;
  175.             }
  176.  
  177.             child = child.parentElement;
  178.         }
  179.  
  180.         return currentParent;
  181.     }
  182.  
  183.     /**
  184.      * Remove all off-canvas states
  185.      */
  186.     function removeAllFocusStates() {
  187.         'use strict';
  188.  
  189.         var siteBranding            = document.getElementsByClassName( 'site-branding' )[0];
  190.         var getFocusedElements      = siteBranding.querySelectorAll(':hover, :focus, :focus-within');
  191.         var getFocusedClassElements = siteBranding.querySelectorAll('.is-focused');
  192.         var i;
  193.         var o;
  194.  
  195.         for ( i = 0; i < getFocusedElements.length; i++) {
  196.             getFocusedElements[i].blur();
  197.         }
  198.  
  199.         for ( o = 0; o < getFocusedClassElements.length; o++) {
  200.             deleteClass( getFocusedClassElements[o], 'is-focused' );
  201.         }
  202.     }
  203.  
  204.     /**
  205.      * Matches polyfill for IE11
  206.      */
  207.     if (!Element.prototype.matches) {
  208.         Element.prototype.matches = Element.prototype.msMatchesSelector;
  209.     }
  210.  
  211.     /**
  212.      * Toggle `focus` class to allow sub-menu access on touch screens.
  213.      */
  214.     function toggleSubmenuDisplay() {
  215.  
  216.         document.addEventListener('touchstart', function(event) {
  217.  
  218.             if ( event.target.matches('a') ) {
  219.  
  220.                 var url = event.target.getAttribute( 'href' ) ? event.target.getAttribute( 'href' ) : '';
  221.  
  222.                 // Open submenu if url is #
  223.                  if ( '#' === url && event.target.nextSibling.matches('.submenu-expand') ) {
  224.                     openSubMenu( event.target );
  225.                 }
  226.  
  227.             }
  228.  
  229.             // Check if .submenu-expand is touched
  230.             if ( event.target.matches('.submenu-expand') ) {
  231.                 openSubMenu(event.target);
  232.  
  233.             // Check if child of .submenu-expand is touched
  234.             } else if ( null != getCurrentParent( event.target, '.submenu-expand' ) && getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) ) {
  235.                 openSubMenu( getCurrentParent( event.target, '.submenu-expand' ) );
  236.  
  237.             // Check if .menu-item-link-return is touched
  238.             } else if ( event.target.matches('.menu-item-link-return') ) {
  239.                 closeSubMenu( event.target );
  240.  
  241.             // Check if child of .menu-item-link-return is touched
  242.             } else if ( null != getCurrentParent( event.target, '.menu-item-link-return' ) && getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) {
  243.                 closeSubMenu( event.target );
  244.             }
  245.  
  246.             // Prevent default mouse/focus events
  247.             removeAllFocusStates();
  248.  
  249.         }, false);
  250.  
  251.         document.addEventListener('touchend', function(event) {
  252.  
  253.             var mainNav = getCurrentParent( event.target, '.main-navigation' );
  254.  
  255.             if ( null != mainNav && hasClass( mainNav, '.main-navigation' ) ) {
  256.                 // Prevent default mouse events
  257.                 event.preventDefault();
  258.             } else if (
  259.                 event.target.matches('.submenu-expand') ||
  260.                 null != getCurrentParent( event.target, '.submenu-expand' ) &&
  261.                 getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) ||
  262.                 event.target.matches('.menu-item-link-return') ||
  263.                 null != getCurrentParent( event.target, '.menu-item-link-return' ) &&
  264.                 getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) {
  265.                     // Prevent default mouse events
  266.                     event.preventDefault();
  267.             }
  268.  
  269.             // Prevent default mouse/focus events
  270.             removeAllFocusStates();
  271.  
  272.         }, false);
  273.  
  274.         document.addEventListener('focus', function(event) {
  275.  
  276.             if ( event.target.matches('.main-navigation > div > ul > li a') ) {
  277.  
  278.                 // Remove Focused elements in sibling div
  279.                 var currentDiv        = getCurrentParent( event.target, 'div', '.main-navigation' );
  280.                 var currentDivSibling = currentDiv.previousElementSibling === null ? currentDiv.nextElementSibling : currentDiv.previousElementSibling;
  281.                 var focusedElement    = currentDivSibling.querySelector( '.is-focused' );
  282.                 var focusedClass      = 'is-focused';
  283.                 var prevLi            = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).previousElementSibling;
  284.                 var nextLi            = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).nextElementSibling;
  285.  
  286.                 if ( null !== focusedElement && null !== hasClass( focusedElement, focusedClass ) ) {
  287.                     deleteClass( focusedElement, focusedClass );
  288.                 }
  289.  
  290.                 // Add .is-focused class to top-level li
  291.                 if ( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ) ) {
  292.                     addClass( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ), focusedClass );
  293.                 }
  294.  
  295.                 // Check for previous li
  296.                 if ( prevLi && hasClass( prevLi, focusedClass ) ) {
  297.                     deleteClass( prevLi, focusedClass );
  298.                 }
  299.  
  300.                 // Check for next li
  301.                 if ( nextLi && hasClass( nextLi, focusedClass ) ) {
  302.                     deleteClass( nextLi, focusedClass );
  303.                 }
  304.             }
  305.  
  306.         }, true);
  307.  
  308.         document.addEventListener('click', function(event) {
  309.  
  310.             // Remove all focused menu states when clicking outside site branding
  311.             if ( event.target !== document.getElementsByClassName( 'site-branding' )[0] ) {
  312.                 removeAllFocusStates();
  313.             } else {
  314.                 // nothing
  315.             }
  316.  
  317.         }, false);
  318.     }
  319.  
  320.     /**
  321.      * Run our sub-menu function as soon as the document is `ready`
  322.      */
  323.     document.addEventListener( 'DOMContentLoaded', function() {
  324.         toggleSubmenuDisplay();
  325.     });
  326.  
  327.     /**
  328.      * Run our sub-menu function on selective refresh in the customizer
  329.      */
  330.     document.addEventListener( 'customize-preview-menu-refreshed', function( e, params ) {
  331.         if ( 'menu-1' === params.wpNavMenuArgs.theme_location ) {
  332.             toggleSubmenuDisplay();
  333.         }
  334.     });
  335.  
  336.     /**
  337.      * Run our sub-menu function every time the window resizes
  338.      */
  339.     var isResizing = false;
  340.     window.addEventListener( 'resize', function() {
  341.         isResizing = true;
  342.         debounce( function() {
  343.             if ( isResizing ) {
  344.                 return;
  345.             }
  346.  
  347.             toggleSubmenuDisplay();
  348.             isResizing = false;
  349.  
  350.         }, 150 );
  351.     } );
  352.  
  353. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement