Current File : /home/honehdyv/readbtooom.com/wp-content/plugins/autodescription/lib/js/tabs.js |
/**
* This file holds The SEO Framework plugin's JS code for the UI Tabs.
* Serve JavaScript as an addition, not as an ends or means.
*
* @author Sybre Waaijer <https://cyberwire.nl/>
* @link <https://wordpress.org/plugins/autodescription/>
*/
/**
* The SEO Framework plugin
* Copyright (C) 2020 - 2023 Sybre Waaijer, CyberWire B.V. (https://cyberwire.nl/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
/**
* Holds tsfTabs values in an object to avoid polluting global namespace.
*
* No jQuery. Cool.
*
* @since 4.1.3
*
* @constructor
*/
window.tsfTabs = function() {
/**
* @since 4.1.3
* @access protected (readonly)
* @see tsfTabs.getStack()
* @type {(Map<string,*>)} The argument stack for ID.
*/
const tabStack = new Map();
/**
* @since 4.1.3
* @access private
* @type {(Map<string,*>)} The cache.
*/
const _toggleCache = new Map();
/**
* Sets the correct tab based on selected radio button prior window.history navigation.
*
* @since 4.1.3
* @access private
*
* @function
* @return {(undefined|null)}
*/
const _correctTabFocus = () => {
let changeEvent = new Event( 'change' );
tabStack.forEach( args => {
if ( ! args.fixHistory ) return;
document.querySelectorAll( `.${args.HTMLClasses.tabRadio}:checked` ).forEach( el => {
el.dispatchEvent( changeEvent );
} );
} );
}
/**
* Toggles a tab instantly, without animations.
*
* @since 4.1.3
* @access public
*
* @function
* @param {string} stackId The stack family ID.
* @param {HTMLInputElement} target The radio input element of the tab.
* @return {(undefined|null)}
*/
const toggleToInstant = ( stackId, target ) => {
const stack = getStack( stackId );
const newContent = document.getElementById( `${target.id}-content` );
if ( newContent && ! newContent.classList.contains( stack.HTMLClasses.activeTabContent ) ) {
// Toggle all active content's HTML classes.
document.querySelectorAll( `.${target.name}-content` ).forEach( element => {
element.classList.remove( stack.HTMLClasses.activeTabContent );
} );
newContent.classList.add( stack.HTMLClasses.activeTabContent );
}
document.getElementById( target.id ).dispatchEvent( stack.tabToggledEvent );
}
/**
* Toggles a tab with animations.
*
* @since 4.1.3
* @access public
*
* @function
* @param {string} stackId The stack family ID.
* @param {HTMLInputElement} target The radio input element of the tab.
* @return {(undefined|null)}
*/
const toggleTo = ( stackId, target ) => {
const cacheId = target.name;
const stack = getStack( stackId );
// TODO make this a public API, and apply to media.js. and tsf.resetAjaxLoader et al.?
// Also add show/hide? -> new tsfUI file? tsfUI.fadeIn( element )
const fadeOutTimeout = 125;
const fadeInTimeout = 175;
const fadeCSS = {
fadeIn: {
opacity: 1,
animation: 'tsf-fade-in',
animationDuration: `${fadeInTimeout}ms`,
animationTimingFunction: 'cubic-bezier(.54,.12,.90,.60)',
},
fadeOut: {
opacity: 0,
animation: 'tsf-fade-out',
animationDuration: `${fadeOutTimeout}ms`,
animationTimingFunction: 'cubic-bezier(.54,.12,.90,.60)',
}
}
const fadeIn = element => { for ( const prop in fadeCSS.fadeIn ) element.style[ prop ] = fadeCSS.fadeIn[ prop ] };
const fadeOut = element => { for ( const prop in fadeCSS.fadeOut ) element.style[ prop ] = fadeCSS.fadeOut[ prop ] };
const container = _toggleCache.get( 'container' ).get( cacheId );
const allContent = document.querySelectorAll( `.${target.name}-content` );
const lockHeight = () => {
container.style.boxSizing = 'border-box'; // FIXME this won't cause issues, right...? Ugh.
container.style.minHeight = `${container.getBoundingClientRect().height}px`;
}
const unLockHeight = () => {
container.style.minHeight = '';
}
const setCorrectTab = async () => {
let newContent = document.getElementById( `${_toggleCache.get( 'target' ).get( cacheId )}-content` );
// Lock height, so to prevent jumping.
lockHeight();
allContent.forEach( el => { el.classList.remove( stack.HTMLClasses.activeTabContent ) } );
newContent.classList.add( stack.HTMLClasses.activeTabContent );
unLockHeight();
fadeIn( newContent );
// Resolve at 2/3th of fade-in time, content should already be well visible.
await new Promise( _resolve => setTimeout( _resolve, fadeInTimeout * 2/3 ) );
return testTab(); // do not pass newContent!
}
const testTab = async () => {
// Regain this value from a new query, for the toggle's target-cache might've changed.
let newContent = document.getElementById( `${_toggleCache.get( 'target' ).get( cacheId )}-content` );
// Test if the correct tab has been set--otherwise, try again.
if ( ! newContent || newContent.classList.contains( stack.HTMLClasses.activeTabContent ) ) {
clearPromise();
document.getElementById( _toggleCache.get( 'target' ).get( cacheId ) )
.dispatchEvent( stack.tabToggledEvent );
} else {
// Lock height isothermically to prevent jumping.
lockHeight();
// Hide everything instantly. We don't make false promises here.
allContent.forEach( el => { el.classList.remove( stack.HTMLClasses.activeTabContent ) } );
// Loop until succesful. Use animationFrame so to not clog up the CPU if this lands in an infinite loop.
requestAnimationFrame( () => {
setCorrectTab() && clearPromise()
} );
}
}
const doPromise = () => new Promise( async resolve => {
allContent.forEach( fadeOut );
// Await fadeout before continuing (with fadeIn at setCorrectTab).
// await setTimeout( resolve, fadeOutTimeout );
await new Promise( _resolve => setTimeout( _resolve, fadeOutTimeout ) );
// resolve();
return setCorrectTab() && resolve();
} );
const clearPromise = () => _toggleCache.get( 'promises' ).delete( cacheId );
const runPromise = () => {
if ( _toggleCache.get( 'promises' ).has( cacheId ) ) return;
_toggleCache.get( 'promises' ).set( cacheId, doPromise );
_toggleCache.get( 'promises' ).get( cacheId )();
}
runPromise();
}
/**
* Toggle tab on tab-radio-change browser event.
*
* @since 4.1.3
* @access private
*
* @function
* @param {*} stackId
* @param {Event} event
* @return {(undefined|null)}
*/
const _toggle = ( stackId, event ) => {
const stack = getStack( stackId );
const currentToggle = event.target;
const onload = ! event.isTrusted;
const toggleId = event.target.id;
const toggleName = event.target.name;
const cacheId = toggleName;
_toggleCache.get( 'wrap' ).has( cacheId ) || (
_toggleCache.get( 'wrap' ).set( cacheId, currentToggle.closest( `.${stack.HTMLClasses.wrapper}` ) )
);
const previousToggle = _toggleCache.get( 'wrap' ).get( cacheId ).querySelector( `.${stack.HTMLClasses.activeTab}` );
if ( ! onload ) {
// Perform validity check, this prevents non-invoking hidden browser validation errors.
// On failure, no tab-switching will happen: the previous tab will become reactivated.
const invalidInput = document.querySelector( `.${stack.HTMLClasses.activeTabContent} :invalid` );
if ( invalidInput ) {
invalidInput.reportValidity();
if ( previousToggle )
previousToggle.checked = true;
currentToggle.checked = false;
event.stopPropagation();
event.preventDefault();
return false;
}
}
if ( previousToggle ) {
previousToggle.classList.remove( stack.HTMLClasses.activeTab );
let label = document.querySelector( `.${stack.HTMLClasses.tabLabel}[for="${previousToggle.id}"]` );
label && label.classList.remove( 'tsf-no-focus-ring' );
}
currentToggle.classList.add( stack.HTMLClasses.activeTab );
if ( onload ) {
toggleToInstant( stackId, event.target );
} else {
if ( ! _toggleCache.get( 'container' ).has( cacheId ) ) {
// instead of 'inside', shouldn't we pick anything blockable?
_toggleCache.get( 'container' ).set( cacheId, currentToggle.closest( '.inside' ) );
}
// Set toggleTarget for (active or upcoming) Promise. This value is set early, so Promises in race conditions will use this.
_toggleCache.get( 'target' ).set( cacheId, toggleId );
// If the promise is running, let it finish and consider the newly set ID.
if ( _toggleCache.get( 'promises' ).has( cacheId ) ) return;
toggleTo( stackId, event.target );
}
}
/**
* Returns the registered stack.
*
* @since 4.1.3
* @access public
* @see tsfTabs.initStack() which registers the stack via param args.
*
* @function
* @param {String} stackId The stack ID for which to get the stack.
* @return {(Object<string, *>)} Immutable Map contents.
*/
const getStack = stackId => tabStack.get( stackId );
/**
* Initializes a tab-switcher stack.
*
* A stack is merely an array of settings. The stack postulates certain events
* and HTMLClasses to be used. However, there may be an unlimited number of
* tabs registered in the DOM, even those with different 'form'-names, acting
* independently with each their own wrapper.
* For an example, see `tsfSettings._initTabs()` and visit the SEO Settings page.
*
* @since 4.1.3
* @access public
* @see tsfTabs.initStack registers the stack.
*
* @function
* @param {String} stackId The stack ID for which to get the stack.
* @param {Object} args The stack arguments.
* @param {CustomEvent} args.tabToggledEvent The Event firing after a tab toggled successfully.
* @param {(Object<string, *>)} args.HTMLClasses The HTML classes pertinent to the stack. Expects:
* wrapper, tabRadio, tabLabel, activeTab, activeTabContent
* @param {Boolean} args.fixHistory Whether to switch tabs based when browser history is used.
* e.g. user hits back-button, non-default-tabRadio is still
* checked, but not switched to correctly. Edge case.
* @return {(undefined|null)}
*/
const initStack = ( stackId, args ) => {
tabStack.set( stackId, args );
const stack = getStack( stackId );
const toggleForwarder = event => _toggle( stackId, event );
const addNoFocusClass = event => event.currentTarget.classList.add( 'tsf-no-focus-ring' );
// Set tab-content on-change.
document.querySelectorAll( `.${stack.HTMLClasses.tabRadio}` ).forEach( el => {
el.addEventListener( 'change', toggleForwarder );
} );
// Prevent focus rings on-click.
document.querySelectorAll( `.${stack.HTMLClasses.wrapper} .${stack.HTMLClasses.tabLabel}` ).forEach( el => {
el.addEventListener( 'click', addNoFocusClass );
} );
}
return Object.assign( {
/**
* Initialises all aspects of the scripts.
* You shouldn't call this.
*
* @since 4.1.3
* @access protected
*
* @function
*/
load: () => {
_toggleCache.set( 'promises', new Map() );
_toggleCache.set( 'target', new Map() );
_toggleCache.set( 'wrap', new Map() );
_toggleCache.set( 'container', new Map() );
// Delay the focus fix, so not to delay the interactive state... even though it causes a page jump. This merely addresses an edge-case.
window.addEventListener( 'load', _correctTabFocus );
}
}, {
toggleToInstant,
toggleTo,
getStack,
initStack,
} );
}();
window.tsfTabs.load();