WordPress/wp-content/themes/twentytwenty/assets/js/index.js

784 lines
24 KiB
JavaScript
Raw Normal View History

/* -----------------------------------------------------------------------------------------------
Namespace
--------------------------------------------------------------------------------------------------- */
var twentytwenty = twentytwenty || {};
// polyfill closest
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
if ( ! Element.prototype.matches ) {
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
}
if ( ! Element.prototype.closest ) {
Element.prototype.closest = function( s ) {
var el = this;
do {
if ( el.matches( s ) ) {
return el;
}
el = el.parentElement || el.parentNode;
} while ( el !== null && el.nodeType === 1 );
return null;
};
}
// polyfill forEach
// https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
if ( window.NodeList && ! NodeList.prototype.forEach ) {
NodeList.prototype.forEach = function( callback, thisArg ) {
var i;
thisArg = thisArg || window;
for ( i = 0; i < this.length; i++ ) {
callback.call( thisArg, this[ i ], i, this );
}
};
}
// event "polyfill"
twentytwenty.createEvent = function( eventName ) {
var event;
if ( typeof window.Event === 'function' ) {
event = new Event( eventName );
} else {
event = document.createEvent( 'Event' );
event.initEvent( eventName, true, false );
}
return event;
};
/* -----------------------------------------------------------------------------------------------
Cover Modals
--------------------------------------------------------------------------------------------------- */
twentytwenty.coverModals = {
init: function() {
if ( document.querySelector( '.cover-modal' ) ) {
// Handle cover modals when they're toggled
this.onToggle();
// When toggled, untoggle if visitor clicks on the wrapping element of the modal
this.outsideUntoggle();
// Close on escape key press
this.closeOnEscape();
// Hide and show modals before and after their animations have played out
this.hideAndShowModals();
}
},
// Handle cover modals when they're toggled
onToggle: function() {
document.querySelector( '.cover-modal' ).addEventListener( 'toggled', function( event ) {
var modal, body;
modal = event.target;
body = document.body;
if ( modal.classList.contains( 'active' ) ) {
body.classList.add( 'showing-modal' );
} else {
body.classList.remove( 'showing-modal' );
body.classList.add( 'hiding-modal' );
// Remove the hiding class after a delay, when animations have been run
setTimeout( function() {
body.classList.remove( 'hiding-modal' );
}, 500 );
}
} );
},
// Close modal on outside click
outsideUntoggle: function() {
document.addEventListener( 'click', function( event ) {
var target = event.target;
var modal = document.querySelector( '.cover-modal.active' );
if ( target === modal ) {
this.untoggleModal( target );
}
}.bind( this ) );
},
// Close modal on escape key press
closeOnEscape: function() {
document.addEventListener( 'keydown', function( event ) {
if ( event.keyCode === 27 ) {
event.preventDefault();
document.querySelectorAll( '.cover-modal.active' ).forEach( function( element ) {
this.untoggleModal( element );
}.bind( this ) );
}
}.bind( this ) );
},
// Hide and show modals before and after their animations have played out
hideAndShowModals: function() {
var modals = document.querySelectorAll( '.cover-modal' ),
htmlStyle = document.documentElement.style;
var getAdminBarHeight = function( negativeValue ) {
var adminBar = document.querySelector( '#wpadminbar' );
if ( adminBar ) {
return ( negativeValue ? '-' : '' ) + adminBar.getBoundingClientRect().height + 'px';
}
return 0;
};
function htmlStyles() {
return {
'overflow-y': 'scroll',
position: 'fixed',
width: '100%',
top: getAdminBarHeight( true ),
left: 0,
};
}
// Show the modal
modals.forEach( function( modal ) {
modal.addEventListener( 'toggle-target-before-inactive', function( event ) {
if ( event.target !== modal ) {
return;
}
window.scrollTo( { top: 0 } );
Object.keys( htmlStyles() ).forEach( function( styleKey ) {
htmlStyle.setProperty( styleKey, htmlStyles()[ styleKey ] );
} );
document.body.style.setProperty( 'padding-top', getAdminBarHeight() );
modal.classList.add( 'show-modal' );
} );
// Hide the modal after a delay, so animations have time to play out
modal.addEventListener( 'toggle-target-after-inactive', function( event ) {
if ( event.target !== modal ) {
return;
}
setTimeout( function() {
modal.classList.remove( 'show-modal' );
Object.keys( htmlStyles() ).forEach( function( styleKey ) {
htmlStyle.removeProperty( styleKey );
} );
document.body.style.removeProperty( 'padding-top' );
}, 500 );
} );
} );
},
// Untoggle a modal
untoggleModal: function( modal ) {
var modalToggle, modalTargetClass;
modalToggle = false;
// If the modal has specified the string (ID or class) used by toggles to target it, untoggle the toggles with that target string
// The modal-target-string must match the string toggles use to target the modal
if ( modal.dataset.modalTargetString ) {
modalTargetClass = modal.dataset.modalTargetString;
modalToggle = document.querySelector( '*[data-toggle-target="' + modalTargetClass + '"]' );
}
// If a modal toggle exists, trigger it so all of the toggle options are included
if ( modalToggle ) {
modalToggle.click();
// If one doesn't exist, just hide the modal
} else {
modal.classList.remove( 'active' );
}
},
}; // twentytwenty.coverModals
/* -----------------------------------------------------------------------------------------------
Focus Management
--------------------------------------------------------------------------------------------------- */
twentytwenty.focusManagement = {
init: function() {
// If the visitor tabs out of the main menu, return focus to the navigation toggle
// Also, if the visitor tabs into a hidden element, move the focus to the element after the hidden element
this.focusLoop();
},
focusLoop: function() {
document.addEventListener( 'focusin', function( event ) {
var element = event.target;
var menuModal = document.querySelector( '.menu-modal' );
var headerToggles = document.querySelector( '.header-toggles' );
var searchModal = document.querySelector( '.search-modal' );
if ( menuModal && menuModal.classList.contains( '.active' ) ) {
if ( ! menuModal.contains( element ) && headerToggles && ! headerToggles.contains( element ) ) {
document.querySelector( '.close-nav-toggle' ).focus();
}
} else if ( searchModal && ! searchModal.classList.contains( '.active' ) ) {
if ( ! searchModal.contains( element ) ) {
searchModal.querySelector( '.search-field' ).focus();
}
}
} );
},
}; // twentytwenty.focusManagement
/* -----------------------------------------------------------------------------------------------
Intrinsic Ratio Embeds
--------------------------------------------------------------------------------------------------- */
twentytwenty.intrinsicRatioVideos = {
init: function() {
this.makeFit();
window.addEventListener( 'fit-videos', function() {
this.makeFit();
}.bind( this ) );
window.addEventListener( 'resize', function() {
this.makeFit();
}.bind( this ) );
},
makeFit: function() {
document.querySelectorAll( 'iframe, object, video' ).forEach( function( video ) {
var container, ratio, iTargetWidth;
container = video.parentNode;
// Skip videos we want to ignore
if ( video.classList.contains( 'intrinsic-ignore' ) || video.parentNode.classList.contains( 'intrinsic-ignore' ) ) {
return true;
}
if ( ! video.dataset.origwidth ) {
// Get the video element proportions
video.setAttribute( 'data-origwidth', video.width );
video.setAttribute( 'data-origheight', video.height );
}
iTargetWidth = container.offsetWidth;
// Get ratio from proportions
ratio = iTargetWidth / video.dataset.origwidth;
// Scale based on ratio, thus retaining proportions
video.style.width = iTargetWidth + 'px';
video.style.height = ( video.dataset.origheight * ratio ) + 'px';
} );
},
}; // twentytwenty.instrinsicRatioVideos
/* -----------------------------------------------------------------------------------------------
Smooth Scroll
--------------------------------------------------------------------------------------------------- */
twentytwenty.smoothScroll = {
init: function() {
// Scroll to anchor
this.scrollToAnchor();
// Scroll to element
this.scrollToElement();
},
// Scroll to anchor
scrollToAnchor: function() {
var anchorElements = document.querySelectorAll( 'a[href*="#"]' );
var anchorElementsList = Array.prototype.slice.call( anchorElements );
anchorElementsList.filter( function( element ) {
if ( element.href === '#' || element.href === '#0' || element.classList.contains( '.do-not-scroll' ) || element.classList.contains( 'skip-link' ) ) {
return false;
}
return true;
} ).forEach( function( element ) {
element.addEventListener( 'click', function( event ) {
var target, scrollOffset, originalOffset, adminBar, scrollSpeed, additionalOffset;
// On-page links
if ( window.location.hostname === event.target.hostname ) {
// Figure out element to scroll to
target = window.location.hash !== '' && document.querySelector( window.location.hash );
target = target ? target : event.target.hash !== '' && document.querySelector( event.target.hash );
// Does a scroll target exist?
if ( target ) {
// Only prevent default if animation is actually gonna happen
event.preventDefault();
// Get options
additionalOffset = event.target.dataset.additionalOffset;
scrollSpeed = event.target.dataset.scrollSpeed ? event.target.dataset.scrollSpeed : 500;
// Determine offset
adminBar = document.querySelector( '#wpadminbar' );
originalOffset = target.getBoundingClientRect().top + window.pageYOffset;
scrollOffset = additionalOffset ? originalOffset + additionalOffset : originalOffset;
if ( adminBar && event.target.className === 'to-the-top' ) {
scrollOffset = scrollOffset - adminBar.getBoundingClientRect().height;
}
twentytwentyScrollTo( scrollOffset, null, scrollSpeed );
window.location.hash = event.target.hash.slice( 1 );
}
}
} );
} );
},
// Scroll to element
scrollToElement: function() {
var scrollToElement = document.querySelector( '*[data-scroll-to]' );
if ( scrollToElement ) {
scrollToElement.addEventListener( 'click', function( event ) {
var target, originalOffset, additionalOffset, scrollOffset, scrollSpeed;
// Figure out element to scroll to
target = event.target.dataset.twentytwentyScrollTo;
// Make sure said element exists
if ( target ) {
event.preventDefault();
// Get options
additionalOffset = event.target.dataset.additionalOffset;
scrollSpeed = event.target.dataset.scrollSpeed ? event.target.dataset.scrollSpeed : 500;
// Determine offset
originalOffset = target.getBoundingClientRect().top + window.pageYOffset;
scrollOffset = additionalOffset ? originalOffset + additionalOffset : originalOffset;
twentytwentyScrollTo( scrollOffset, null, scrollSpeed );
}
} );
}
},
}; // twentytwenty.smoothScroll
/* -----------------------------------------------------------------------------------------------
Main Menu
--------------------------------------------------------------------------------------------------- */
twentytwenty.modalMenu = {
init: function() {
// If the current menu item is in a sub level, expand all the levels higher up on load
this.expandLevel();
},
expandLevel: function() {
var modalMenu = document.querySelector( '.modal-menu' );
var activeMenuItem = modalMenu.querySelector( '.current-menu-item' );
if ( activeMenuItem ) {
twentytwentyFindParents( activeMenuItem, 'li' ).forEach( function( element ) {
var subMenuToggle = element.querySelector( '.sub-menu-toggle' );
if ( subMenuToggle ) {
subMenuToggle.click();
}
} );
}
},
}; // twentytwenty.modalMenu
/* -----------------------------------------------------------------------------------------------
Toggles
--------------------------------------------------------------------------------------------------- */
twentytwenty.toggles = {
init: function() {
// Do the toggle
this.toggle();
// Check for toggle/untoggle on resize
this.resizeCheck();
// Check for untoggle on escape key press
this.untoggleOnEscapeKeyPress();
},
// Do the toggle
toggle: function() {
document.querySelectorAll( '*[data-toggle-target]' ).forEach( function( element ) {
element.addEventListener( 'click', function() {
var toggle, targetString, target, timeOutTime, classToToggle, activeClass;
// Get our targets
toggle = element;
targetString = toggle.dataset.toggleTarget;
activeClass = 'active';
if ( targetString === 'next' ) {
target = toggle.nextSibling;
} else {
target = document.querySelector( targetString );
}
// Trigger events on the toggle targets before they are toggled
if ( target.classList.contains( activeClass ) ) {
target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-before-active' ) );
} else {
target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-before-inactive' ) );
}
// Get the class to toggle, if specified
classToToggle = toggle.dataset.classToToggle ? toggle.dataset.classToToggle : activeClass;
// For cover modals, set a short timeout duration so the class animations have time to play out
timeOutTime = 0;
if ( target.classList.contains( 'cover-modal' ) ) {
timeOutTime = 10;
}
setTimeout( function() {
var focusElement, duration, newTarget, subMenued;
// Toggle the target of the clicked toggle
if ( toggle.dataset.toggleType === 'slidetoggle' ) {
duration = toggle.dataset.toggleDuration ? toggle.dataset.toggleDuration : 250;
subMenued = target.classList.contains( 'sub-menu' );
newTarget = subMenued ? toggle.closest( '.menu-item' ).querySelector( '.sub-menu' ) : target;
twentytwentySlideToggle( newTarget, duration );
} else {
target.classList.toggle( classToToggle );
}
// If the toggle target is 'next', only give the clicked toggle the active class
if ( targetString === 'next' ) {
toggle.classList.toggle( activeClass );
} else if ( target.classList.contains( 'sub-menu' ) ) {
toggle.classList.toggle( activeClass );
} else {
// If not, toggle all toggles with this toggle target
document.querySelector( '*[data-toggle-target="' + targetString + '"]' ).classList.toggle( activeClass );
}
// Toggle aria-expanded on the target
twentytwentyToggleAttribute( target, 'aria-expanded', 'true', 'false' );
// Toggle aria-expanded on the toggle
twentytwentyToggleAttribute( toggle, 'aria-expanded', 'true', 'false' );
// Toggle body class
if ( toggle.dataset.toggleBodyClass ) {
document.querySelector( 'body' ).classList.toggle( toggle.dataset.toggleBodyClass );
}
// Check whether to set focus
if ( toggle.dataset.setFocus ) {
focusElement = document.querySelector( toggle.dataset.setFocus );
if ( focusElement ) {
if ( target.classList.contains( activeClass ) ) {
focusElement.focus();
} else {
focusElement.blur();
}
}
}
// Trigger the toggled event on the toggle target
target.dispatchEvent( twentytwenty.createEvent( 'toggled' ) );
// Trigger events on the toggle targets after they are toggled
if ( target.classList.contains( activeClass ) ) {
target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-after-active' ) );
} else {
target.dispatchEvent( twentytwenty.createEvent( 'toggle-target-after-inactive' ) );
}
}, timeOutTime );
} );
} );
},
// Check for toggle/untoggle on screen resize
resizeCheck: function() {
if ( document.querySelectorAll( '*[data-untoggle-above], *[data-untoggle-below], *[data-toggle-above], *[data-toggle-below]' ).length ) {
window.addEventListener( 'resize', function() {
var winWidth = window.innerWidth,
toggles = document.querySelectorAll( '.toggle' );
toggles.forEach( function( toggle ) {
var unToggleAbove = toggle.dataset.untoggleAbove,
unToggleBelow = toggle.dataset.untoggleBelow,
toggleAbove = toggle.dataset.toggleAbove,
toggleBelow = toggle.dataset.toggleBelow;
// If no width comparison is set, continue
if ( ! unToggleAbove && ! unToggleBelow && ! toggleAbove && ! toggleBelow ) {
return;
}
// If the toggle width comparison is true, toggle the toggle
if (
( ( ( unToggleAbove && winWidth > unToggleAbove ) ||
( unToggleBelow && winWidth < unToggleBelow ) ) &&
toggle.classList.contains( 'active' ) ) ||
( ( ( toggleAbove && winWidth > toggleAbove ) ||
( toggleBelow && winWidth < toggleBelow ) ) &&
! toggle.classList.contains( 'active' ) )
) {
toggle.click();
}
} );
} );
}
},
// Close toggle on escape key press
untoggleOnEscapeKeyPress: function() {
document.addEventListener( 'keyup', function( event ) {
if ( event.key === 'Escape' ) {
document.querySelectorAll( '*[data-untoggle-on-escape].active' ).forEach( function( element ) {
if ( element.classList.contains( 'active' ) ) {
element.click();
}
} );
}
} );
},
}; // twentytwenty.toggles
/**
* Is the DOM ready
*
* this implementation is coming from https://gomakethings.com/a-native-javascript-equivalent-of-jquerys-ready-method/
*
* @param {Function} fn Callback function to run.
*/
function twentytwentyDomReady( fn ) {
if ( typeof fn !== 'function' ) {
return;
}
if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
return fn();
}
document.addEventListener( 'DOMContentLoaded', fn, false );
}
twentytwentyDomReady( function() {
twentytwenty.toggles.init(); // Handle toggles
twentytwenty.coverModals.init(); // Handle cover modals
twentytwenty.intrinsicRatioVideos.init(); // Retain aspect ratio of videos on window resize
twentytwenty.smoothScroll.init(); // Smooth scroll to anchor link or a specific element
twentytwenty.modalMenu.init(); // Modal Menu
twentytwenty.focusManagement.init(); // Focus Management
} );
/* -----------------------------------------------------------------------------------------------
Helper functions
--------------------------------------------------------------------------------------------------- */
/* Toggle an attribute ----------------------- */
function twentytwentyToggleAttribute( element, attribute, trueVal, falseVal ) {
if ( trueVal === undefined ) {
trueVal = true;
}
if ( falseVal === undefined ) {
falseVal = false;
}
if ( element[ attribute ] !== trueVal ) {
element.setAttribute( attribute, trueVal );
} else {
element.setAttribute( attribute, falseVal );
}
}
/**
* twentytwentySlideUp
*
* this implementation is coming from https://w3bits.com/javascript-slidetoggle/
*
* @param {HTMLElement} target
* @param {number} duration
*/
function twentytwentySlideUp( target, duration ) {
target.style.transitionProperty = 'height, margin, padding'; /* [1.1] */
target.style.transitionDuration = duration + 'ms'; /* [1.2] */
target.style.boxSizing = 'border-box'; /* [2] */
target.style.height = target.offsetHeight + 'px'; /* [3] */
target.style.height = 0; /* [4] */
target.style.paddingTop = 0; /* [5.1] */
target.style.paddingBottom = 0; /* [5.2] */
target.style.marginTop = 0; /* [6.1] */
target.style.marginBottom = 0; /* [7.2] */
target.style.overflow = 'hidden'; /* [7] */
window.setTimeout( function() {
target.style.display = 'none'; /* [8] */
target.style.removeProperty( 'height' ); /* [9] */
target.style.removeProperty( 'padding-top' ); /* [10.1] */
target.style.removeProperty( 'padding-bottom' ); /* [10.2] */
target.style.removeProperty( 'margin-top' ); /* [11.1] */
target.style.removeProperty( 'margin-bottom' ); /* [11.2] */
target.style.removeProperty( 'overflow' ); /* [12] */
target.style.removeProperty( 'transition-duration' ); /* [13.1] */
target.style.removeProperty( 'transition-property' ); /* [13.2] */
}, duration );
}
/**
* twentytwentySlideDown
*
* this implementation is coming from https://w3bits.com/javascript-slidetoggle/
*
* @param {HTMLElement} target
* @param {number} duration
*/
function twentytwentySlideDown( target, duration ) {
var height, display;
target.style.removeProperty( 'display' ); /* [1] */
display = window.getComputedStyle( target ).display;
if ( display === 'none' ) { /* [2] */
display = 'block';
}
target.style.display = display;
height = target.offsetHeight; /* [3] */
target.style.height = 0; /* [4] */
target.style.paddingTop = 0; /* [5.1] */
target.style.paddingBottom = 0; /* [5.2] */
target.style.marginTop = 0; /* [6.1] */
target.style.marginBottom = 0; /* [6.2] */
target.style.overflow = 'hidden'; /* [7] */
target.style.boxSizing = 'border-box'; /* [8] */
target.style.transitionProperty = 'height, margin, padding'; /* [9.1] */
target.style.transitionDuration = duration + 'ms'; /* [9.2] */
target.style.height = height + 'px'; /* [10] */
target.style.removeProperty( 'padding-top' ); /* [11.1] */
target.style.removeProperty( 'padding-bottom' ); /* [11.2] */
target.style.removeProperty( 'margin-top' ); /* [12.1] */
target.style.removeProperty( 'margin-bottom' ); /* [12.2] */
window.setTimeout( function() {
target.style.removeProperty( 'height' ); /* [13] */
target.style.removeProperty( 'overflow' ); /* [14] */
target.style.removeProperty( 'transition-duration' ); /* [15.1] */
target.style.removeProperty( 'transition-property' ); /* [15.2] */
}, duration );
}
/**
* twentytwentySlideToggle
*
* this implementation is coming from https://w3bits.com/javascript-slidetoggle/
*
* @param {HTMLElement} target
* @param {number} duration
*/
function twentytwentySlideToggle( target, duration ) {
if ( duration === undefined ) {
duration = 500;
}
if ( window.getComputedStyle( target ).display === 'none' ) {
return twentytwentySlideDown( target, duration );
}
return twentytwentySlideUp( target, duration );
}
/**
* traverses the DOM up to find elements matching the query
*
* @param {HTMLElement} target
* @param {string} query
* @return {NodeList} parents matching query
*/
function twentytwentyFindParents( target, query ) {
var parents = [];
// recursively go up the DOM adding matches to the parents array
function traverse( item ) {
var parent = item.parentNode;
if ( parent instanceof HTMLElement ) {
if ( parent.matches( query ) ) {
parents.push( parent );
}
traverse( parent );
}
}
traverse( target );
return parents;
}
// twentytwentyEaseInOutQuad functions http://goo.gl/5HLl8
function twentytwentyEaseInOutQuad( t, b, c, d ) {
t /= d / 2;
if ( t < 1 ) {
return ( ( ( c / 2 ) * t ) * t ) + b;
}
t--;
return ( ( -c / 2 ) * ( ( t * ( t - 2 ) ) - 1 ) ) + b;
}
function twentytwentyScrollTo( to, callback, duration ) {
var start, change, increment, currentTime;
function move( amount ) {
document.documentElement.scrollTop = amount;
document.body.parentNode.scrollTop = amount;
document.body.scrollTop = amount;
}
start = document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop;
change = to - start;
increment = 20;
currentTime = 0;
duration = ( typeof ( duration ) === 'undefined' ) ? 500 : duration;
function animateScroll() {
var val;
// increment the time
currentTime += increment;
// find the value with the quadratic in-out twentytwentyEaseInOutQuad function
val = twentytwentyEaseInOutQuad( currentTime, start, change, duration );
// move the document.body
move( val );
// do the animation unless its over
if ( currentTime < duration ) {
window.requestAnimationFrame( animateScroll );
} else if ( callback && typeof ( callback ) === 'function' ) {
// the animation is done so lets callback
callback();
}
}
animateScroll();
}