diff options
Diffstat (limited to 'themes/twentynineteen/js')
-rw-r--r-- | themes/twentynineteen/js/customize-controls.js | 30 | ||||
-rw-r--r-- | themes/twentynineteen/js/customize-preview.js | 60 | ||||
-rw-r--r-- | themes/twentynineteen/js/priority-menu.js | 216 | ||||
-rw-r--r-- | themes/twentynineteen/js/skip-link-focus-fix.js | 33 | ||||
-rw-r--r-- | themes/twentynineteen/js/touch-keyboard-navigation.js | 354 |
5 files changed, 693 insertions, 0 deletions
diff --git a/themes/twentynineteen/js/customize-controls.js b/themes/twentynineteen/js/customize-controls.js new file mode 100644 index 00000000..e717e909 --- /dev/null +++ b/themes/twentynineteen/js/customize-controls.js @@ -0,0 +1,30 @@ +/** + * File customizer.js. + * + * Theme Customizer enhancements for a better user experience. + * + * Contains handlers to make Theme Customizer preview reload changes asynchronously. + */ + +(function() { + + wp.customize.bind( 'ready', function() { + + // Only show the color hue control when there's a custom primary color. + wp.customize( 'primary_color', function( setting ) { + wp.customize.control( 'primary_color_hue', function( control ) { + var visibility = function() { + if ( 'custom' === setting.get() ) { + control.container.slideDown( 180 ); + } else { + control.container.slideUp( 180 ); + } + }; + + visibility(); + setting.bind( visibility ); + }); + }); + }); + +})( jQuery ); diff --git a/themes/twentynineteen/js/customize-preview.js b/themes/twentynineteen/js/customize-preview.js new file mode 100644 index 00000000..60dfa6fc --- /dev/null +++ b/themes/twentynineteen/js/customize-preview.js @@ -0,0 +1,60 @@ +/** + * File customizer.js. + * + * Theme Customizer enhancements for a better user experience. + * + * Contains handlers to make Theme Customizer preview reload changes asynchronously. + */ + +(function( $ ) { + + // Primary color. + wp.customize( 'primary_color', function( value ) { + value.bind( function( to ) { + // Update custom color CSS. + var style = $( '#custom-theme-colors' ), + hue = style.data( 'hue' ), + css = style.html(), + color; + + if( 'custom' === to ){ + // If a custom primary color is selected, use the currently set primary_color_hue + color = wp.customize.get().primary_color_hue; + } else { + // If the "default" option is selected, get the default primary_color_hue + color = _TwentyNineteenPreviewData.default_hue; + } + + // Equivalent to css.replaceAll, with hue followed by comma to prevent values with units from being changed. + css = css.split( hue + ',' ).join( color + ',' ); + style.html( css ).data( 'hue', color ); + }); + }); + + // Primary color hue. + wp.customize( 'primary_color_hue', function( value ) { + value.bind( function( to ) { + + // Update custom color CSS. + var style = $( '#custom-theme-colors' ), + hue = style.data( 'hue' ), + css = style.html(); + + // Equivalent to css.replaceAll, with hue followed by comma to prevent values with units from being changed. + css = css.split( hue + ',' ).join( to + ',' ); + style.html( css ).data( 'hue', to ); + }); + }); + + // Image filter. + wp.customize( 'image_filter', function( value ) { + value.bind( function( to ) { + if ( to ) { + $( 'body' ).addClass( 'image-filters-enabled' ); + } else { + $( 'body' ).removeClass( 'image-filters-enabled' ); + } + } ); + } ); + +})( jQuery ); diff --git a/themes/twentynineteen/js/priority-menu.js b/themes/twentynineteen/js/priority-menu.js new file mode 100644 index 00000000..7cd6bb06 --- /dev/null +++ b/themes/twentynineteen/js/priority-menu.js @@ -0,0 +1,216 @@ +(function() { + + /** + * Debounce + * + * @param {Function} func + * @param {number} wait + * @param {boolean} immediate + */ + function debounce(func, wait, immediate) { + 'use strict'; + + var timeout; + wait = (typeof wait !== 'undefined') ? wait : 20; + immediate = (typeof immediate !== 'undefined') ? immediate : true; + + return function() { + + var context = this, args = arguments; + var later = function() { + timeout = null; + + if (!immediate) { + func.apply(context, args); + } + }; + + var callNow = immediate && !timeout; + + clearTimeout(timeout); + timeout = setTimeout(later, wait); + + if (callNow) { + func.apply(context, args); + } + }; + } + + /** + * Prepends an element to a container. + * + * @param {Element} container + * @param {Element} element + */ + function prependElement(container, element) { + if (container.firstChild.nextSibling) { + return container.insertBefore(element, container.firstChild.nextSibling); + } else { + return container.appendChild(element); + } + } + + /** + * Shows an element by adding a hidden className. + * + * @param {Element} element + */ + function showButton(element) { + // classList.remove is not supported in IE11 + element.className = element.className.replace('is-empty', ''); + } + + /** + * Hides an element by removing the hidden className. + * + * @param {Element} element + */ + function hideButton(element) { + // classList.add is not supported in IE11 + if (!element.classList.contains('is-empty')) { + element.className += ' is-empty'; + } + } + + /** + * Returns the currently available space in the menu container. + * + * @returns {number} Available space + */ + function getAvailableSpace( button, container ) { + return container.offsetWidth - button.offsetWidth - 22; + } + + /** + * Returns whether the current menu is overflowing or not. + * + * @returns {boolean} Is overflowing + */ + function isOverflowingNavivation( list, button, container ) { + return list.offsetWidth > getAvailableSpace( button, container ); + } + + /** + * Set menu container variable + */ + var navContainer = document.querySelector('.main-navigation'); + var breaks = []; + + /** + * Let’s bail if we our menu doesn't exist + */ + if ( ! navContainer ) { + return; + } + + /** + * Refreshes the list item from the menu depending on the menu size + */ + function updateNavigationMenu( container ) { + + /** + * Let’s bail if our menu is empty + */ + if ( ! container.parentNode.querySelector('.main-menu[id]') ) { + return; + } + + // Adds the necessary UI to operate the menu. + var visibleList = container.parentNode.querySelector('.main-menu[id]'); + var hiddenList = visibleList.parentNode.nextElementSibling.querySelector('.hidden-links'); + var toggleButton = visibleList.parentNode.nextElementSibling.querySelector('.main-menu-more-toggle'); + + if ( isOverflowingNavivation( visibleList, toggleButton, container ) ) { + + // Record the width of the list + breaks.push( visibleList.offsetWidth ); + // Move last item to the hidden list + prependElement( hiddenList, ! visibleList.lastChild || null === visibleList.lastChild ? visibleList.previousElementSibling : visibleList.lastChild ); + // Show the toggle button + showButton( toggleButton ); + + } else { + + // There is space for another item in the nav + if ( getAvailableSpace( toggleButton, container ) > breaks[breaks.length - 1] ) { + // Move the item to the visible list + visibleList.appendChild( hiddenList.firstChild.nextSibling ); + breaks.pop(); + } + + // Hide the dropdown btn if hidden list is empty + if (breaks.length < 2) { + hideButton( toggleButton ); + } + } + + // Recur if the visible list is still overflowing the nav + if ( isOverflowingNavivation( visibleList, toggleButton, container ) ) { + updateNavigationMenu( container ); + } + } + + /** + * Run our priority+ function as soon as the document is `ready` + */ + document.addEventListener( 'DOMContentLoaded', function() { + + updateNavigationMenu( navContainer ); + + // Also, run our priority+ function on selective refresh in the customizer + var hasSelectiveRefresh = ( + 'undefined' !== typeof wp && + wp.customize && + wp.customize.selectiveRefresh && + wp.customize.navMenusPreview.NavMenuInstancePartial + ); + + if ( hasSelectiveRefresh ) { + // Re-run our priority+ function on Nav Menu partial refreshes + wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function ( placement ) { + + var isNewNavMenu = ( + placement && + placement.partial.id.includes( 'nav_menu_instance' ) && + 'null' !== placement.container[0].parentNode && + placement.container[0].parentNode.classList.contains( 'main-navigation' ) + ); + + if ( isNewNavMenu ) { + updateNavigationMenu( placement.container[0].parentNode ); + } + }); + } + }); + + /** + * Run our priority+ function on load + */ + window.addEventListener( 'load', function() { + updateNavigationMenu( navContainer ); + }); + + /** + * Run our priority+ function every time the window resizes + */ + var isResizing = false; + window.addEventListener( 'resize', + debounce( function() { + if ( isResizing ) { + return; + } + + isResizing = true; + setTimeout( function() { + updateNavigationMenu( navContainer ); + isResizing = false; + }, 150 ); + } ) + ); + + /** + * Run our priority+ function + */ + updateNavigationMenu( navContainer ); + +})(); diff --git a/themes/twentynineteen/js/skip-link-focus-fix.js b/themes/twentynineteen/js/skip-link-focus-fix.js new file mode 100644 index 00000000..32ba80cc --- /dev/null +++ b/themes/twentynineteen/js/skip-link-focus-fix.js @@ -0,0 +1,33 @@ +/** + * File skip-link-focus-fix.js. + * + * Helps with accessibility for keyboard only users. + * + * This is the source file for what is minified in the twentynineteen_skip_link_focus_fix() PHP function. + * + * Learn more: https://git.io/vWdr2 + */ +( function() { + var isIe = /(trident|msie)/i.test( navigator.userAgent ); + + if ( isIe && document.getElementById && window.addEventListener ) { + window.addEventListener( 'hashchange', function() { + var id = location.hash.substring( 1 ), + element; + + if ( ! ( /^[A-z0-9_-]+$/.test( id ) ) ) { + return; + } + + element = document.getElementById( id ); + + if ( element ) { + if ( ! ( /^(?:a|select|input|button|textarea)$/i.test( element.tagName ) ) ) { + element.tabIndex = -1; + } + + element.focus(); + } + }, false ); + } +} )(); diff --git a/themes/twentynineteen/js/touch-keyboard-navigation.js b/themes/twentynineteen/js/touch-keyboard-navigation.js new file mode 100644 index 00000000..2fa19056 --- /dev/null +++ b/themes/twentynineteen/js/touch-keyboard-navigation.js @@ -0,0 +1,354 @@ +/** + * Touch & Keyboard navigation. + * + * Contains handlers for touch devices and keyboard navigation. + */ + +(function() { + + /** + * Debounce + * + * @param {Function} func + * @param {number} wait + * @param {boolean} immediate + */ + function debounce(func, wait, immediate) { + 'use strict'; + + var timeout; + wait = (typeof wait !== 'undefined') ? wait : 20; + immediate = (typeof immediate !== 'undefined') ? immediate : true; + + return function() { + + var context = this, args = arguments; + var later = function() { + timeout = null; + + if (!immediate) { + func.apply(context, args); + } + }; + + var callNow = immediate && !timeout; + + clearTimeout(timeout); + timeout = setTimeout(later, wait); + + if (callNow) { + func.apply(context, args); + } + }; + } + + /** + * Add class + * + * @param {Object} el + * @param {string} cls + */ + function addClass(el, cls) { + if ( ! el.className.match( '(?:^|\\s)' + cls + '(?!\\S)') ) { + el.className += ' ' + cls; + } + } + + /** + * Delete class + * + * @param {Object} el + * @param {string} cls + */ + function deleteClass(el, cls) { + el.className = el.className.replace( new RegExp( '(?:^|\\s)' + cls + '(?!\\S)' ),'' ); + } + + /** + * Has class? + * + * @param {Object} el + * @param {string} cls + * + * @returns {boolean} Has class + */ + function hasClass(el, cls) { + + if ( el.className.match( '(?:^|\\s)' + cls + '(?!\\S)' ) ) { + return true; + } + } + + /** + * Toggle Aria Expanded state for screenreaders + * + * @param {Object} ariaItem + */ + function toggleAriaExpandedState( ariaItem ) { + 'use strict'; + + var ariaState = ariaItem.getAttribute('aria-expanded'); + + if ( ariaState === 'true' ) { + ariaState = 'false'; + } else { + ariaState = 'true'; + } + + ariaItem.setAttribute('aria-expanded', ariaState); + } + + /** + * Open sub-menu + * + * @param {Object} currentSubMenu + */ + function openSubMenu( currentSubMenu ) { + 'use strict'; + + // Update classes + // classList.add is not supported in IE11 + currentSubMenu.parentElement.className += ' off-canvas'; + currentSubMenu.parentElement.lastElementChild.className += ' expanded-true'; + + // Update aria-expanded state + toggleAriaExpandedState( currentSubMenu ); + } + + /** + * Close sub-menu + * + * @param {Object} currentSubMenu + */ + function closeSubMenu( currentSubMenu ) { + 'use strict'; + + var menuItem = getCurrentParent( currentSubMenu, '.menu-item' ); // this.parentNode + var menuItemAria = menuItem.querySelector('a[aria-expanded]'); + var subMenu = currentSubMenu.closest('.sub-menu'); + + // If this is in a sub-sub-menu, go back to parent sub-menu + if ( getCurrentParent( currentSubMenu, 'ul' ).classList.contains( 'sub-menu' ) ) { + + // Update classes + // classList.remove is not supported in IE11 + menuItem.className = menuItem.className.replace( 'off-canvas', '' ); + subMenu.className = subMenu.className.replace( 'expanded-true', '' ); + + // Update aria-expanded and :focus states + toggleAriaExpandedState( menuItemAria ); + + // Or else close all sub-menus + } else { + + // Update classes + // classList.remove is not supported in IE11 + menuItem.className = menuItem.className.replace( 'off-canvas', '' ); + menuItem.lastElementChild.className = menuItem.lastElementChild.className.replace( 'expanded-true', '' ); + + // Update aria-expanded and :focus states + toggleAriaExpandedState( menuItemAria ); + } + } + + /** + * Find first ancestor of an element by selector + * + * @param {Object} child + * @param {String} selector + * @param {String} stopSelector + */ + function getCurrentParent( child, selector, stopSelector ) { + + var currentParent = null; + + while ( child ) { + + if ( child.matches(selector) ) { + + currentParent = child; + break; + + } else if ( stopSelector && child.matches(stopSelector) ) { + + break; + } + + child = child.parentElement; + } + + return currentParent; + } + + /** + * Remove all off-canvas states + */ + function removeAllFocusStates() { + 'use strict'; + + var siteBranding = document.getElementsByClassName( 'site-branding' )[0]; + var getFocusedElements = siteBranding.querySelectorAll(':hover, :focus, :focus-within'); + var getFocusedClassElements = siteBranding.querySelectorAll('.is-focused'); + var i; + var o; + + for ( i = 0; i < getFocusedElements.length; i++) { + getFocusedElements[i].blur(); + } + + for ( o = 0; o < getFocusedClassElements.length; o++) { + deleteClass( getFocusedClassElements[o], 'is-focused' ); + } + } + + /** + * Matches polyfill for IE11 + */ + if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector; + } + + /** + * Toggle `focus` class to allow sub-menu access on touch screens. + */ + function toggleSubmenuDisplay() { + + document.addEventListener('touchstart', function(event) { + + if ( event.target.matches('a') ) { + + var url = event.target.getAttribute( 'href' ) ? event.target.getAttribute( 'href' ) : ''; + + // Open submenu if url is # + if ( '#' === url && event.target.nextSibling.matches('.submenu-expand') ) { + openSubMenu( event.target ); + } + } + + // Check if .submenu-expand is touched + if ( event.target.matches('.submenu-expand') ) { + openSubMenu(event.target); + + // Check if child of .submenu-expand is touched + } else if ( null != getCurrentParent( event.target, '.submenu-expand' ) && + getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) ) { + openSubMenu( getCurrentParent( event.target, '.submenu-expand' ) ); + + // Check if .menu-item-link-return is touched + } else if ( event.target.matches('.menu-item-link-return') ) { + closeSubMenu( event.target ); + + // Check if child of .menu-item-link-return is touched + } else if ( null != getCurrentParent( event.target, '.menu-item-link-return' ) && getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) { + closeSubMenu( event.target ); + } + + // Prevent default mouse/focus events + removeAllFocusStates(); + + }, false); + + document.addEventListener('touchend', function(event) { + + var mainNav = getCurrentParent( event.target, '.main-navigation' ); + + if ( null != mainNav && hasClass( mainNav, '.main-navigation' ) ) { + // Prevent default mouse events + event.preventDefault(); + + } else if ( + event.target.matches('.submenu-expand') || + null != getCurrentParent( event.target, '.submenu-expand' ) && + getCurrentParent( event.target, '.submenu-expand' ).matches( '.submenu-expand' ) || + event.target.matches('.menu-item-link-return') || + null != getCurrentParent( event.target, '.menu-item-link-return' ) && + getCurrentParent( event.target, '.menu-item-link-return' ).matches( '.menu-item-link-return' ) ) { + // Prevent default mouse events + event.preventDefault(); + } + + // Prevent default mouse/focus events + removeAllFocusStates(); + + }, false); + + document.addEventListener('focus', function(event) { + + if ( event.target.matches('.main-navigation > div > ul > li a') ) { + + // Remove Focused elements in sibling div + var currentDiv = getCurrentParent( event.target, 'div', '.main-navigation' ); + var currentDivSibling = currentDiv.previousElementSibling === null ? currentDiv.nextElementSibling : currentDiv.previousElementSibling; + var focusedElement = currentDivSibling.querySelector( '.is-focused' ); + var focusedClass = 'is-focused'; + var prevLi = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).previousElementSibling; + var nextLi = getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ).nextElementSibling; + + if ( null !== focusedElement && null !== hasClass( focusedElement, focusedClass ) ) { + deleteClass( focusedElement, focusedClass ); + } + + // Add .is-focused class to top-level li + if ( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ) ) { + addClass( getCurrentParent( event.target, '.main-navigation > div > ul > li', '.main-navigation' ), focusedClass ); + } + + // Check for previous li + if ( prevLi && hasClass( prevLi, focusedClass ) ) { + deleteClass( prevLi, focusedClass ); + } + + // Check for next li + if ( nextLi && hasClass( nextLi, focusedClass ) ) { + deleteClass( nextLi, focusedClass ); + } + } + + }, true); + + document.addEventListener('click', function(event) { + + // Remove all focused menu states when clicking outside site branding + if ( event.target !== document.getElementsByClassName( 'site-branding' )[0] ) { + removeAllFocusStates(); + } else { + // nothing + } + + }, false); + } + + /** + * Run our sub-menu function as soon as the document is `ready` + */ + document.addEventListener( 'DOMContentLoaded', function() { + toggleSubmenuDisplay(); + }); + + /** + * Run our sub-menu function on selective refresh in the customizer + */ + document.addEventListener( 'customize-preview-menu-refreshed', function( e, params ) { + if ( 'menu-1' === params.wpNavMenuArgs.theme_location ) { + toggleSubmenuDisplay(); + } + }); + + /** + * Run our sub-menu function every time the window resizes + */ + var isResizing = false; + window.addEventListener( 'resize', function() { + isResizing = true; + debounce( function() { + if ( isResizing ) { + return; + } + + toggleSubmenuDisplay(); + isResizing = false; + + }, 150 ); + } ); + +})(); |