File: /home/thuexe247c/public_html/wp-content/plugins/autodescription/lib/js/settings.js
/**
* This file holds The SEO Framework plugin's JS code for the SEO Settings page.
* 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) 2019 - 2024 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 tsfSettings values in an object to avoid polluting global namespace.
*
* @since 4.0.0
* TODO split up this file?
*
* @constructor
* @param {!jQuery} $ jQuery object.
*/
window.tsfSettings = function( $ ) {
/**
* Data property injected by WordPress l10n handler.
*
* @since 4.0.0
* @access public
* @type {(Object<string,*>)|boolean|null} l10n Localized strings
*/
const l10n = tsfSettingsL10n;
/**
* Returns settings ID.
*
* @since 4.1.1
* @access private
*
* @param {string} name
* @return {string} The full settings ID/name.
*/
function _getSettingsId( name ) {
return `autodescription-site-settings[${name}]`;
}
/**
* Clone of tsf.dispatchAtInteractive.
* Eases programming, trims minified script size.
*
* @since 4.2.0
* @access private
* @ignore
*
* @function
* @param {Element} element The element to dispatch the event upon.
* @param {string} eventName The event name to trigger. Mustn't be custom.
*/
const _dispatchAtInteractive = tsf.dispatchAtInteractive;
/**
* Initializes the settings page.
*
* @since 5.1.0
* @access private
*/
function _initSubmit() {
const form = document.getElementById( 'tsf-settings' );
// How?
if ( ! form ) return;
const submitButtons = form.querySelectorAll( '[name=submit]' );
// Prevent double-submit.
form.addEventListener(
'submit',
() => {
submitButtons.forEach( el => el.disabled = true );
setTimeout( () => submitButtons.forEach( el => el.disabled = false ), 3000 );
},
);
}
/**
* Initializes input helpers for the General Settings.
*
* @since 4.0.0
* @access private
*/
function _initGeneralSettings() {
/**
* Triggers displaying/hiding of character counters on the settings page.
*
* @since 4.1.1
* @access private
*
* @function
* @param {Event} event
*/
const toggleCharCounterDisplay = event => {
document.querySelectorAll( '.tsf-counter-wrap' ).forEach( el => {
el.style.display = event.target.checked ? '' : 'none';
} );
event.target.checked && tsfC?.triggerCounterUpdate();
}
document.getElementById( _getSettingsId( 'display_character_counter' ) )
?.addEventListener( 'click', toggleCharCounterDisplay );
/**
* Triggers displaying/hiding of pixel counters on the settings page.
*
* @since 4.0.0
* @access private
*
* @function
* @param {Event} event
*/
const togglePixelCounterDisplay = event => {
document.querySelectorAll( '.tsf-pixel-counter-wrap' ).forEach( el => {
el.style.display = event.target.checked ? '' : 'none';
} );
event.target.checked && tsfC?.triggerCounterUpdate();
}
document.getElementById( _getSettingsId( 'display_pixel_counter' ) )
?.addEventListener( 'click', togglePixelCounterDisplay );
/**
* Emits a canonical URL scheme change to rewrite canonical URLs on the page.
*
* @since 5.1.0
* @access private
*
* @function
* @param {Event} event
*/
const dispatchCanonicalSchemeUpdate = event => {
const selected = event.target.value,
values = JSON.parse( event.target.dataset?.values || 0 ) || []; // not cached OK.
document.body.dispatchEvent( new CustomEvent(
'tsf-canonical-scheme-changed',
{
detail: {
scheme: values[ selected ] ?? selected,
},
},
) );
}
document.getElementById( _getSettingsId( 'canonical_scheme' ) )
?.addEventListener( 'change', dispatchCanonicalSchemeUpdate );
const excludedPostTypes = new Set(), // Excluded post types.
excludedTaxonomies = new Set(), // Excluded taxonomies.
excludedPtTaxonomies = new Set(), // Excluded taxonomies via post types.
excludedTaxonomiesAll = new Set(); // Combined E_Taxonomies + E_PtTaxonomies
const validateTaxonomyState = () => {
// We want to show that the taxonomy is excluded, but make that auto-reversible, and somehow still enactable?
let taxEntries = document.querySelectorAll( '.tsf-excluded-taxonomies' ),
triggerchange = false;
taxEntries.forEach( element => {
// get taxonomy from last [] entry.
const taxonomy = element.name.split( /(?:.+\[)(.+?)(?:])/ ).join( '' );
const taxPostTypes = JSON.parse( element.dataset?.postTypes || 0 ) || [],
isDisabled = taxPostTypes && taxPostTypes.every( postType => excludedPostTypes.has( postType ) );
if ( isDisabled ) {
if ( ! excludedPtTaxonomies.has( taxonomy ) ) {
// Newly disabled, trigger change.
triggerchange = true;
}
// Filter it out to prevent duplicates. Redundant?
excludedPtTaxonomies.add( taxonomy );
} else {
if ( excludedPtTaxonomies.has( taxonomy ) ) {
excludedPtTaxonomies.delete( taxonomy );
// Enabled again, was disabled. Trigger change.
triggerchange = true;
}
}
refreshTaxonomies();
triggerchange && dispatchTaxonomySupportChangedEvent( taxonomy );
} );
}
document.body.addEventListener( 'tsf-post-type-support-changed', validateTaxonomyState );
const refreshTaxonomies = () => {
// Refresh and concatenate.
excludedTaxonomiesAll.clear();
excludedTaxonomies.forEach( taxonomy => excludedTaxonomiesAll.add( taxonomy ) );
excludedPtTaxonomies.forEach( taxonomy => excludedTaxonomiesAll.add( taxonomy ) );
}
const dispatchTaxonomySupportChangedEvent = taxonomy => {
document.body.dispatchEvent( new CustomEvent(
'tsf-taxonomy-support-changed',
{
detail: {
taxonomy,
set: excludedTaxonomies,
setPt: excludedPtTaxonomies,
setAll: excludedTaxonomiesAll,
}
}
) );
}
const dispatchPosttypeSupportChangedEvent = postType => {
document.body.dispatchEvent( new CustomEvent(
'tsf-post-type-support-changed',
{
detail: {
postType,
set: excludedPostTypes,
}
}
) );
}
// This prevents notice-removal checks before they're added.
let init = false;
const checkDisabledPT = event => {
if ( ! event.target.name ) return;
// get post type from last [] entry.
let postType = event.target.name.split( /(?:.+\[)(.+?)(?:])/ ).join( '' );
if ( event.target.checked ) {
excludedPostTypes.add( postType );
dispatchPosttypeSupportChangedEvent( postType );
} else {
// No need to filter when it was never registered in the first place.
if ( init ) {
excludedPostTypes.delete( postType );
dispatchPosttypeSupportChangedEvent( postType );
}
}
}
const checkDisabledTaxonomy = event => {
if ( ! event.target.name ) return;
// get taxonomy from last [] entry.
let taxonomy = event.target.name.split( /(?:.+\[)(.+?)(?:])/ ).join( '' );
if ( event.target.checked ) {
excludedTaxonomies.add( taxonomy );
refreshTaxonomies();
dispatchTaxonomySupportChangedEvent( taxonomy );
} else {
// No need to filter when it was never registered in the first place.
if ( init ) {
excludedTaxonomies.delete( taxonomy );
refreshTaxonomies();
dispatchTaxonomySupportChangedEvent( taxonomy );
}
}
}
document.querySelectorAll( '.tsf-excluded-post-types' ).forEach( el => {
el.addEventListener( 'change', checkDisabledPT );
_dispatchAtInteractive( el, 'change' );
} );
document.querySelectorAll( '.tsf-excluded-taxonomies' ).forEach( el => {
el.addEventListener( 'change', checkDisabledTaxonomy );
_dispatchAtInteractive( el, 'change' );
} );
init = true;
}
/**
* Enables wpColorPicker on input.
*
* @since 4.0.0
* @access private
*/
function _initColorPicker() {
document.querySelectorAll( '.tsf-color-picker' ).forEach( element => {
// We might as well switch to jQuery instantly since wpColorPicker added its prototype to it.
let $input = $( element ),
currentColor = '',
defaultColor = $input.data( 'tsf-default-color' );
$input.wpColorPicker( {
defaultColor: defaultColor,
width: 238,
change: () => {
currentColor = $input.wpColorPicker( 'color' );
if ( '' === currentColor )
currentColor = defaultColor;
element.value = defaultColor;
tsfAys.registerChange();
},
clear: () => {
// We can't loop this to the change method, as it's not reliable (due to deferring?).
// So, we just fill it in for the user.
if ( defaultColor.length ) {
element.value = defaultColor;
$input.closest( '.wp-picker-container' ).find( '.wp-color-result' ).css( 'backgroundColor', defaultColor );
}
tsfAys.registerChange();
},
palettes: false,
} );
} );
}
/**
* Initializes Titles' meta input.
*
* @since 4.0.0
* @since 4.0.5 Fixed the additionsToggle getter.
* @access private
*/
function _initTitleSettings() {
const additionsToggle = document.getElementById( _getSettingsId( 'title_rem_additions' ) ),
socialAdditionsToggle = document.getElementById( _getSettingsId( 'social_title_rem_additions' ) ),
titleAdditionsHelpTemplate = wp.template( 'tsf-disabled-title-additions-help-social' )();
/**
* Toggles example on Left/Right selection of global title options.
*
* @function
*/
const toggleAdditionsDisplayExample = event => {
if ( event.target.checked ) {
document.querySelectorAll( '.tsf-title-additions-js' ).forEach( el => el.style.display = 'none' );
if ( socialAdditionsToggle ) {
socialAdditionsToggle.dataset.disabledWarning = 1;
socialAdditionsToggle.closest( 'label' ).insertAdjacentHTML( 'beforeend', titleAdditionsHelpTemplate );
tsfTT.triggerReset();
}
} else {
document.querySelectorAll( '.tsf-title-additions-js' ).forEach( el => el.style.display = 'inline' );
// 'tsf-title-additions-warning-social' is defined at `../inc/views/templates/settings/settings.php`
if ( socialAdditionsToggle?.dataset.disabledWarning )
socialAdditionsToggle.closest( 'label' ).querySelector( '.tsf-title-additions-warning-social' )?.remove();
}
document.body.dispatchEvent( new CustomEvent(
'tsf-update-title-rem-additions',
{
detail: {
removeAdditions: !! event.target.checked,
}
}
) );
}
if ( additionsToggle ) {
additionsToggle.addEventListener( 'change', toggleAdditionsDisplayExample );
_dispatchAtInteractive( additionsToggle, 'change' );
}
/**
* Toggles title additions location for the Title examples.
* There are two elements, rather than one. One is hidden by default.
*
* @function
* @param {Event} event
*/
const toggleAdditionsLocationExample = event => {
let value;
document.getElementsByName( event.target.name ).forEach( el => {
if ( el.checked )
value = el.value;
} );
const showLeft = 'left' === value,
locationClass = 'tsf-title-additions-location-hidden';
document.querySelectorAll( '.tsf-title-additions-example-left' ).forEach( el => {
el.classList.toggle( locationClass, ! showLeft );
el.classList.remove( 'hidden' );
} );
document.querySelectorAll( '.tsf-title-additions-example-right' ).forEach( el => {
el.classList.toggle( locationClass, showLeft );
el.classList.remove( 'hidden' );
} );
tsfTitle.updateStateAll(
'additionPlacement',
showLeft ? 'before' : 'after',
_getSettingsId( 'homepage_title' )
);
}
document.querySelectorAll( '#tsf-title-location input' ).forEach( el => {
el.addEventListener( 'change', toggleAdditionsLocationExample );
if ( el.checked )
_dispatchAtInteractive( el, 'change' );
} );
/**
* Toggles title prefixes for the Prefix Title example.
*
* @function
* @param {Event} event
*/
const adjustPrefixExample = event => {
const showPrefix = ! event.target.checked,
prefixClass = 'tsf-title-tax-prefix-hidden';
document.querySelectorAll( '.tsf-title-tax-prefix' ).forEach( el => {
el.classList.toggle( prefixClass, ! showPrefix );
el.classList.remove( 'hidden' );
} );
document.querySelectorAll( '.tsf-title-tax-noprefix' ).forEach( el => {
el.classList.toggle( prefixClass, showPrefix );
el.classList.remove( 'hidden' );
} );
tsfTitle.updateStateAll( 'showPrefix', showPrefix, _getSettingsId( 'homepage_title' ) );
}
const titleRemPrefixes = document.getElementById( _getSettingsId( 'title_rem_prefixes' ) );
if ( titleRemPrefixes ) {
titleRemPrefixes.addEventListener( 'change', adjustPrefixExample );
_dispatchAtInteractive( titleRemPrefixes, 'change' );
}
/**
* Updates used separator and all examples thereof.
*
* @function
* @param {Event} event
*/
const updateSeparator = event => {
const separator = tsf.decodeEntities( event.target.dataset.entity ),
activeClass = 'tsf-title-separator-active';
document.querySelectorAll( '.tsf-sep-js' ).forEach( el => {
el.textContent = ` ${separator} `; // two spaces hug it.
} );
window.dispatchEvent(
new CustomEvent(
'tsf-title-sep-updated',
{
detail: { separator }
}
)
);
let oldActiveLabel = document.querySelector( `.${activeClass}` );
oldActiveLabel && oldActiveLabel.classList.remove( activeClass, 'tsf-no-focus-ring' );
let activeLabel = document.querySelector( `label[for="${event.target.id}"]` );
activeLabel && activeLabel.classList.add( activeClass );
}
document.querySelectorAll( '#tsf-title-separator input' ).forEach( el => {
el.addEventListener( 'click', updateSeparator );
} );
/**
* Sets a class to the active element which helps excluding focus rings.
*
* @function
* @param {Event} event
*/
const addNoFocusClass = event => {
event.target.classList.add( 'tsf-no-focus-ring' );
}
document.querySelectorAll( '#tsf-title-separator label' ).forEach( el => {
el.addEventListener( 'click', addNoFocusClass );
} );
const homeTitleId = _getSettingsId( 'homepage_title' ),
siteTitleInput = document.getElementById( _getSettingsId( 'site_title' ) );
/**
* Adjusts homepage left/right title example part.
*
* @function
* @param {Event} event
*/
const adjustSiteTitleExampleOutput = event => {
let examples = document.querySelectorAll( '.tsf-site-title-js' ),
newVal = tsf.decodeEntities( tsf.sDoubleSpace( event.target.value.trim() ) );
newVal ||= tsf.decodeEntities( event.target.placeholder );
// If the home-as-page has a title, don't overwrite.
if ( ! tsfTitle.getStateOf( homeTitleId, '_defaultTitleLocked' ) )
tsfTitle.updateStateOf( homeTitleId, 'defaultTitle', newVal );
tsfTitle.updateStateAll( 'additionValue', newVal, homeTitleId );
let htmlVal = tsf.escapeString( newVal );
examples.forEach( el => { el.innerHTML = htmlVal } );
}
if ( siteTitleInput ) {
siteTitleInput.addEventListener( 'input', adjustSiteTitleExampleOutput );
_dispatchAtInteractive( siteTitleInput, 'input' );
}
}
/**
* Initializes home's general tab meta input listeners.
*
* @since 4.0.0
* @access private
*/
function _initHomeGeneralListeners() {
/**
* Enqueues meta title and description input triggers.
* These triggers force an update for the counters without affecting tsfAys.
*
* @function
*/
const enqueueGeneralInputListeners = () => {
tsfTitle.enqueueUnregisteredInputTrigger( _getSettingsId( 'homepage_title' ) );
tsfDescription.enqueueUnregisteredInputTrigger( _getSettingsId( 'homepage_description' ) );
}
/**
* Enqueues doctitles input trigger synchronously on postbox collapse or open.
*
* @function
* @param {!jQuery.Event} event
* @param {Element} elem
*/
const triggerPostboxSynchronousUnregisteredInput = ( event, elem ) => {
if ( 'autodescription-homepage-settings' === elem.id ) {
let inside = elem.querySelector( '.inside' );
if ( inside.offsetHeight > 0 && inside.offsetWidth > 0 ) {
enqueueGeneralInputListeners();
}
}
}
// jQuery: WP action.
$( document ).on( 'postbox-toggled', triggerPostboxSynchronousUnregisteredInput );
// This also triggers change for the homepage description, which isn't necessary. But, this trims down codebase.
document.getElementById( 'tsf-homepage-tab-general' )
?.addEventListener( 'tsf-tab-toggled', enqueueGeneralInputListeners );
}
/**
* Initializes Homepage's meta title input.
*
* @since 4.0.0
* @since 4.2.8 Now parses custom state _defaultTitleLocked.
* @access private
*/
function _initHomeTitleSettings() {
const _titleId = _getSettingsId( 'homepage_title' );
const titleInput = document.getElementById( _titleId ),
taglineInput = document.getElementById( _getSettingsId( 'homepage_title_tagline' ) ),
taglineToggle = document.getElementById( _getSettingsId( 'homepage_tagline' ) );
if ( ! titleInput ) return;
tsfTitle.setInputElement( titleInput );
const state = JSON.parse(
document.getElementById( `tsf-title-data_${_titleId}` )?.dataset.state || 0,
);
tsfTitle.updateStateOf( _titleId, 'allowReferenceChange', ! state.refTitleLocked );
tsfTitle.updateStateOf( _titleId, 'defaultTitle', state.defaultTitle );
tsfTitle.updateStateOf( _titleId, 'addAdditions', state.addAdditions );
tsfTitle.updateStateOf( _titleId, 'useSocialTagline', !! ( state.useSocialTagline || false ) );
tsfTitle.updateStateOf( _titleId, 'additionValue', state.additionValue );
tsfTitle.updateStateOf( _titleId, 'additionPlacement', state.additionPlacement );
tsfTitle.updateStateOf( _titleId, '_defaultTitleLocked', !! ( state._defaultTitleLocked || false ) );
tsfTitle.enqueueUnregisteredInputTrigger( _titleId );
/**
* Updates the hover additions placement.
*
* @since 4.1.1
*
* @function
*/
const toggleHoverAdditionsPlacement = event => {
tsfTitle.updateStateOf(
_titleId,
'additionPlacement',
'left' === event.target.value ? 'before' : 'after',
);
}
document.querySelectorAll( '#tsf-home-title-location input' ).forEach( el => {
el.addEventListener( 'change', toggleHoverAdditionsPlacement );
if ( el.checked )
_dispatchAtInteractive( el, 'change' );
} );
/**
* Sets private/protected visibility state.
*
* @function
* @param {string} visibility
*/
const setTitleVisibilityPrefix = visibility => {
let oldPrefixValue = tsfTitle.getStateOf( _titleId, 'prefixValue' ),
prefixValue = '';
switch ( visibility ) {
case 'password':
prefixValue = tsfTitle.protectedPrefix;
break;
case 'private':
prefixValue = tsfTitle.privatePrefix;
break;
default:
case 'public':
prefixValue = '';
break;
}
if ( prefixValue !== oldPrefixValue )
tsfTitle.updateStateOf( _titleId, 'prefixValue', prefixValue );
}
if ( l10n.states.isFrontPrivate ) {
setTitleVisibilityPrefix( 'private' );
} else if ( l10n.states.isFrontProtected ) {
setTitleVisibilityPrefix( 'password' );
}
/**
* Adjusts homepage left/right title example part.
*
* @function
* @param {Event} event
*/
const adjustHomepageExampleOutput = event => {
let examples = document.querySelectorAll( '.tsf-custom-title-js' ),
val = tsf.decodeEntities( tsf.sDoubleSpace( event.target.value.trim() ) );
if ( val.length ) {
val = tsf.escapeString( val );
examples.forEach( el => el.innerHTML = val );
} else {
val = tsf.escapeString( tsf.decodeEntities( tsfTitle.getStateOf( _titleId, 'defaultTitle' ) ) );
examples.forEach( el => el.innerHTML = val );
}
}
titleInput.addEventListener( 'input', adjustHomepageExampleOutput );
_dispatchAtInteractive( titleInput, 'input' );
let updateHomePageTaglineExampleOutputBuffer;
/**
* Updates homepage title example output.
*
* @function
*/
const updateHomePageTaglineExampleOutput = () => {
clearTimeout( updateHomePageTaglineExampleOutputBuffer );
updateHomePageTaglineExampleOutputBuffer = setTimeout(
() => {
let value = tsfTitle.getStateOf( _titleId, 'additionValue' );
value = tsf.decodeEntities( tsf.sDoubleSpace( value.trim() ) );
if ( value.length && tsfTitle.getStateOf( _titleId, 'addAdditions' ) ) {
document.querySelectorAll( '.tsf-custom-tagline-js' ).forEach( el => {
el.innerHTML = tsf.escapeString( value );
} );
document.querySelectorAll( '.tsf-custom-blogname-js' ).forEach( el => {
el.style.display = null;
} );
} else {
document.querySelectorAll( '.tsf-custom-blogname-js' ).forEach( el => {
el.style.display = 'none';
} );
}
},
1000/60 // 60fps.
);
}
/**
* Updates the hover additions value.
*
* @function
*/
const updateHoverAdditionsValue = () => {
let value = taglineInput.value.trim();
if ( ! value.length )
value = taglineInput.placeholder || '';
value = tsf.escapeString( tsf.decodeEntities( value.trim() ) );
tsfTitle.updateStateOf( _titleId, 'additionValue', value );
updateHomePageTaglineExampleOutput();
}
taglineInput.addEventListener( 'input', updateHoverAdditionsValue );
_dispatchAtInteractive( taglineInput, 'input' );
/**
* Toggle tagline end examples within the Left/Right example for the homepage titles.
* Also disables the input field for extra clarity.
*
* @function
* @param {Event} event
*/
const toggleHomePageTaglineExampleDisplay = event => {
let addAdditions = false;
if ( event.target.checked ) {
addAdditions = true;
taglineInput.readOnly = false;
} else {
addAdditions = false;
taglineInput.readOnly = true;
}
// A change action implies a change. Don't test for previous; it changed!
// (also, it defaults to false; which would cause a bug not calling updateHomePageTaglineExampleOutput on-load)
tsfTitle.updateStateOf( _titleId, 'addAdditions', addAdditions );
updateHomePageTaglineExampleOutput();
}
taglineToggle.addEventListener( 'change', toggleHomePageTaglineExampleDisplay );
_dispatchAtInteractive( taglineToggle, 'change' );
/**
* Updates separator used in the titles.
*
* @function
* @param {Event} event
*/
const updateSeparator = event => {
tsfTitle.updateStateAll( 'separator', event.detail.separator );
}
window.addEventListener( 'tsf-title-sep-updated', updateSeparator );
}
/**
* Initializes Homepage's meta description input.
*
* @since 4.0.0
* @access private
*/
function _initHomeDescriptionSettings() {
const descId = _getSettingsId( 'homepage_description' );
tsfDescription.setInputElement( document.getElementById( descId ) );
const state = JSON.parse(
document.getElementById( `tsf-description-data_${descId}` )?.dataset.state || 0,
);
if ( state ) {
// tsfDescription.updateState( 'allowReferenceChange', ! state.refDescriptionLocked );
tsfDescription.updateStateOf( descId, 'defaultDescription', state.defaultDescription.trim() );
}
tsfDescription.enqueueUnregisteredInputTrigger( descId );
}
/**
* Initializes Homepage's social meta input.
*
* @since 4.2.0
* @access private
*/
function _initHomeSocialSettings() {
const _socialGroup = 'homepage_social_settings';
tsfSocial.setInputInstance(
_socialGroup,
_getSettingsId( 'homepage_title' ),
_getSettingsId( 'homepage_description' ),
);
const groupData = JSON.parse(
document.getElementById( `tsf-social-data_${_socialGroup}` )?.dataset.settings || 0,
);
if ( ! groupData ) return;
tsfSocial.updateStateOf( _socialGroup, 'addAdditions', groupData.og.state.addAdditions ); // tw Also has one. Maybe future.
tsfSocial.updateStateOf(
_socialGroup,
'defaults',
{
ogTitle: groupData.og.state.defaultTitle,
twTitle: groupData.tw.state.defaultTitle,
ogDesc: groupData.og.state.defaultDesc,
twDesc: groupData.tw.state.defaultDesc,
}
);
tsfSocial.updateStateOf(
_socialGroup,
'placeholderLocks',
{
ogTitle: groupData.og.state?.titlePhLock || false,
twTitle: groupData.tw.state?.titlePhLock || false,
ogDesc: groupData.og.state?.descPhLock || false,
twDesc: groupData.tw.state?.descPhLock || false,
}
);
const twitterCardType = document.getElementById( _getSettingsId( 'homepage_twitter_card_type' ) );
const updateTitleRemoveAdditions = event => {
const { cardType } = event.detail;
const _defaultIndexOption = twitterCardType.querySelector( '[value=""]' ),
_data = twitterCardType.dataset || {};
const newHTML = _data.defaultI18n?.replace(
'%s',
_data.defaultLocked
? _data.defaultValue
: cardType,
);
_defaultIndexOption.innerHTML = newHTML;
twitterCardType.dispatchEvent( new Event( 'change' ) );
}
if ( twitterCardType )
document.body.addEventListener( 'tsf-update-twitter-card-type', updateTitleRemoveAdditions );
}
/**
* Initializes Homepage's visibility input.
*
* @since 5.1.0
* @access private
*/
function _initHomeVisibilitySettings() {
const _canonicalId = _getSettingsId( 'homepage_canonical' ),
_noindexId = _getSettingsId( 'homepage_noindex' );
const canonicalInput = document.getElementById( _canonicalId ),
noindexInput = document.getElementById( _noindexId );
if ( ! canonicalInput ) return;
// Prefixed with B because I don't trust using 'protected' (might become reserved).
const BNOINDEX = 0b10;
let canonicalPhState = 0b00;
tsfCanonical.setInputElement( canonicalInput );
const state = JSON.parse( document.getElementById( `tsf-canonical-data_${_canonicalId}` )?.dataset.state || 0 );
if ( state ) {
tsfCanonical.updateStateOf( _canonicalId, 'allowReferenceChange', ! state.refCanonicalLocked );
tsfCanonical.updateStateOf( _canonicalId, 'defaultCanonical', state.defaultCanonical.trim() );
tsfCanonical.updateStateOf( _canonicalId, 'preferredScheme', state.preferredScheme.trim() );
tsfCanonical.updateStateOf( _canonicalId, 'urlStructure', state.urlStructure );
}
tsfCanonical.enqueueTriggerUnregisteredInput( _canonicalId );
document.body.addEventListener( 'tsf-canonical-scheme-changed', event => {
tsfCanonical.updateStateOf( _canonicalId, 'preferredScheme', event.detail.scheme );
} );
/**
* @since 5.1.0
*
* @function
*/
const updateCanonicalPlaceholder = () => {
tsfCanonical.updateStateOf(
_canonicalId,
'showUrlPlaceholder',
canonicalPhState & BNOINDEX
? false
: true,
);
}
updateCanonicalPlaceholder();
let pageNoindex = false,
siteNoindex = false;
const updateNoindexState = () => {
let type = 'index';
switch ( state.noindexQubit ) {
case 0: // default, unset since unknown.
if ( noindexInput?.checked || siteNoindex || pageNoindex || state.isProtected ) {
type = 'noindex';
} else {
type = 'index';
}
break;
case -1: // force index
type = 'index';
break;
case 1: // force noindex
type = 'noindex';
}
if ( 'noindex' === type ) {
canonicalPhState |= BNOINDEX;
} else {
canonicalPhState &= ~BNOINDEX;
}
updateCanonicalPlaceholder();
}
noindexInput?.addEventListener( 'change', updateNoindexState );
if ( state.isPage ) {
const checkPTNoindex = event => {
const { robotsType, set } = event.detail;
if ( 'noindex' !== robotsType ) return;
pageNoindex = set.has( 'page' );
updateNoindexState();
}
document.body.addEventListener( 'tsf-post-type-robots-changed', checkPTNoindex );
}
const checkSiteNoindex = event => {
const { checked, robotsType } = event.detail;
if ( 'noindex' !== robotsType ) return;
siteNoindex = !! checked;
updateNoindexState();
}
document.body.addEventListener( 'tsf-site-robots-changed', checkSiteNoindex );
}
/**
* Returns the option name/id of PTA settings.
*
* @since 4.2.0
* @access private
*
* @param {String} postType
* @param {String} id
* @return {String} The option name/id.
*/
function _getPtaInputId( postType, id ) {
return `${_getSettingsId('pta')}[${postType}][${id}]`;
}
let _cachedPtaData = void 0;
/**
* Returns predefined PTA object data.
*
* @since 4.2.0
* @access private
*
* @param {string|undefined} postType
* @return {{label:string,url:string,hasPosts:boolean}}
*/
function _getPtaData() {
return _cachedPtaData ||= JSON.parse(
document.getElementById( 'tsf-post-type-archive-data' )?.dataset.postTypes || 0
) || {};
}
/**
* Initializes all Post Type Archive setting fields.
*
* @since 4.2.0
* @access private
*/
function _initPtaSettings() {
const postTypeData = _getPtaData(),
itemLength = Object.keys( postTypeData ).length;
switch ( true ) {
case itemLength > 1:
_initPtaSelector();
// fall through;
case itemLength > 0:
_initPtaListeners();
break;
default:
break;
}
// Yes, this will spawn many event listeners if there are many post type archives.
// I call those 'Event Horizon cases'. Puns very much intended.
for ( const postType in postTypeData ) {
_initPtaTitleSettings( postType );
_initPtaDescriptionSettings( postType );
_initPtaSocialSettings( postType );
_initPtaVisibilitySettings( postType );
_initPtaMainListeners( postType );
}
}
/**
* Initializes the Post Type Archive selector/switcher.
*
* @since 4.2.0
* @access private
*/
function _initPtaSelector() {
const postTypeData = _getPtaData();
const select = document.getElementById( 'tsf-post-type-archive-selector' ),
optionOption = document.createElement( 'option' );
const headerWrap = document.getElementById( 'tsf-post-type-archive-header-wrap' );
headerWrap && ( headerWrap.style.display = null );
const populateSelect = () => {
for ( const postType in postTypeData ) {
let _option = optionOption.cloneNode();
_option.value = tsf.escapeString( postType );
_option.innerHTML = tsf.escapeString( postTypeData[ postType ].label );
select?.appendChild( _option );
}
}
populateSelect();
// Hide all headers.
document.querySelectorAll( '.tsf-post-type-header' ).forEach( el => el.classList.add( 'hidden' ) );
let _debounceSwitch = void 0,
_detailsEl;
const switchPostTypeSettingsView = event => {
clearTimeout( _debounceSwitch );
_debounceSwitch = setTimeout(
() => {
// Remove old details (if any).
_detailsEl && headerWrap?.removeChild( _detailsEl );
document.querySelectorAll( '.tsf-post-type-archive-wrap' ).forEach( el => {
if ( event.target.value === el.dataset.postType ) {
el.style.display = null;
_detailsEl = el.querySelector( '.tsf-post-type-archive-details' )?.cloneNode( true );
} else {
el.style.display = 'none';
}
// This class is redundant now; remove it for it hides permanently.
el.classList.remove( 'hide-if-tsf-js' );
} );
_detailsEl && headerWrap?.appendChild( _detailsEl );
document.body.dispatchEvent(
new CustomEvent( 'tsf-post-type-archive-switched', {
detail: {
postType: event.target.value,
hasKompaanChocolateBananaBeer: false, // sad day.
}
} )
);
},
1000/60, // 60fps.
);
}
if ( select ) {
select.addEventListener( 'change', switchPostTypeSettingsView );
_dispatchAtInteractive( select, 'change' );
}
}
/**
* Initializes the global Post Type Archive listeners.
*
* @since 4.2.0
* @access private
*/
function _initPtaListeners() {
const augmentSwitcher = event => {
const { postType, set } = event.detail,
wrap = document.querySelector( `.tsf-post-type-archive-wrap[data-post-type="${postType}"]` ),
excluded = set.has( postType );
wrap?.querySelector( '.tsf-post-type-archive-if-excluded' )?.classList.toggle( 'hidden', ! excluded );
wrap?.querySelector( '.tsf-post-type-archive-if-not-excluded' )?.classList.toggle( 'hidden', excluded );
document.body.dispatchEvent(
// Necessary to trigger input events
new CustomEvent( 'tsf-post-type-archive-switched', {
detail: {
postType: postType,
}
} )
);
}
// This also dispatches at Interactive.
document.body.addEventListener( 'tsf-post-type-support-changed', augmentSwitcher );
}
/**
* Initializes PTA's meta title input.
*
* @since 4.2.0
* @access private
*
* @param {String} postType The post type name.
*/
function _initPtaTitleSettings( postType ) {
const _titleId = _getPtaInputId( postType, 'doctitle' ),
titleInput = document.getElementById( _titleId );
if ( ! titleInput ) return;
tsfTitle.setInputElement( titleInput );
const state = JSON.parse(
document.getElementById( `tsf-title-data_${_titleId}` )?.dataset.state || 0,
);
if ( state ) {
tsfTitle.updateStateOf( _titleId, 'defaultTitle', state.defaultTitle );
tsfTitle.updateStateOf( _titleId, 'addAdditions', state.addAdditions );
tsfTitle.updateStateOf( _titleId, 'useSocialTagline', !! ( state.useSocialTagline || false ) );
tsfTitle.updateStateOf( _titleId, 'additionValue', state.additionValue );
tsfTitle.updateStateOf( _titleId, 'additionPlacement', state.additionPlacement );
tsfTitle.updateStateOf( _titleId, 'prefixValue', state.prefixValue );
tsfTitle.updateStateOf( _titleId, 'showPrefix', state.showPrefix );
}
/**
* Updates title prefix, based on input and global settings.
*
* @function
* @param {Event} event
*/
const updateTitlePrefix = event => {
let showPrefix = ! event.target.value.trim().length;
if ( document.getElementById( _getSettingsId( 'title_rem_prefixes' ) )?.checked )
showPrefix = false;
tsfTitle.updateStateOf( _titleId, 'showPrefix', showPrefix );
}
titleInput.addEventListener( 'input', updateTitlePrefix );
/**
* Updates title additions, based on singular settings change.
*
* @function
* @param {Event} event
*/
const updateTitleAdditions = event => {
let addAdditions = ! event.target.checked;
if ( document.getElementById( _getSettingsId( 'title_rem_additions' ) )?.checked )
addAdditions = false;
tsfTitle.updateStateOf( _titleId, 'addAdditions', addAdditions );
}
const disabledTitleAdditionsHelp = wp.template( 'tsf-disabled-title-additions-help' )();
const blogNameTrigger = document.getElementById( _getPtaInputId( postType, 'title_no_blog_name' ) );
const updateTitleRemoveAdditions = event => {
const { removeAdditions } = event.detail;
blogNameTrigger.disabled = removeAdditions;
if ( removeAdditions ) {
blogNameTrigger.closest( 'label' ).insertAdjacentHTML( 'beforeend', disabledTitleAdditionsHelp );
tsfTT.triggerReset();
} else {
// 'tsf-title-additions-warning' is defined at `../inc/views/templates/settings/settings.php`
blogNameTrigger.closest( 'label' ).querySelector( '.tsf-title-additions-warning' )?.remove();
}
blogNameTrigger.dispatchEvent( new Event( 'change' ) );
}
if ( blogNameTrigger ) {
document.body.addEventListener( 'tsf-update-title-rem-additions', updateTitleRemoveAdditions );
blogNameTrigger.addEventListener( 'change', updateTitleAdditions );
_dispatchAtInteractive( blogNameTrigger, 'change' );
}
tsfTitle.enqueueUnregisteredInputTrigger( _titleId );
}
/**
* Initializes PTA's meta description input.
*
* @since 4.2.0
* @access private
*
* @param {String} postType The post type name.
*/
function _initPtaDescriptionSettings( postType ) {
const _descId = _getPtaInputId( postType, 'description' ),
descInput = document.getElementById( _descId );
if ( ! descInput ) return;
tsfDescription.setInputElement( descInput );
const state = JSON.parse(
document.getElementById( `tsf-description-data_${_descId}` )?.dataset.state || 0,
);
if ( state )
tsfDescription.updateStateOf( _descId, 'defaultDescription', state.defaultDescription.trim() );
tsfDescription.enqueueUnregisteredInputTrigger( _descId );
}
/**
* Initializes PTA's social meta input.
*
* @since 4.2.0
* @access private
*
* @param {String} postType The post type name.
*/
function _initPtaSocialSettings( postType ) {
const _socialGroup = `pta_social_settings_${postType}`;
const groupData = JSON.parse(
document.getElementById( `tsf-social-data_${_socialGroup}` )?.dataset.settings || 0,
);
tsfSocial.setInputInstance(
_socialGroup,
_getPtaInputId( postType, 'doctitle' ),
_getPtaInputId( postType, 'description' ),
);
tsfSocial.updateStateOf( _socialGroup, 'addAdditions', groupData.og.state.addAdditions ); // tw Also has one. Maybe future.
tsfSocial.updateStateOf(
_socialGroup,
'defaults',
{
ogTitle: groupData.og.state.defaultTitle,
twTitle: groupData.tw.state.defaultTitle,
ogDesc: groupData.og.state.defaultDesc,
twDesc: groupData.tw.state.defaultDesc,
}
);
const twitterCardType = document.getElementById( _getPtaInputId( postType, 'tw_card_type' ) );
const updateTitleRemoveAdditions = event => {
const { cardType } = event.detail;
const _defaultIndexOption = twitterCardType.querySelector( '[value=""]' ),
_data = twitterCardType.dataset || {};
const newHTML = _data.defaultI18n?.replace(
'%s',
cardType,
);
_defaultIndexOption.innerHTML = newHTML;
twitterCardType.dispatchEvent( new Event( 'change' ) );
}
if ( twitterCardType )
document.body.addEventListener( 'tsf-update-twitter-card-type', updateTitleRemoveAdditions );
}
/**
* Initializes PTA's Visibility input.
*
* @since 4.2.0
* @access private
*
* @param {String} postType The post type name.
*/
function _initPtaVisibilitySettings( postType ) {
const canonicalId = _getPtaInputId( postType, 'canonical' ),
noindexId = _getPtaInputId( postType, 'noindex' );
const canonicalInput = document.getElementById( canonicalId ),
indexSelect = document.getElementById( noindexId );
// Prefixed with B because I don't trust using 'protected' (might become reserved).
const BNOINDEX = 0b10;
let canonicalPhState = 0b00;
/**
* @since 5.1.0
*
* @function
*/
const updateCanonicalPlaceholder = () => {
tsfCanonical.updateStateOf(
canonicalId,
'showUrlPlaceholder',
canonicalPhState & BNOINDEX
? false
: true,
);
}
if ( canonicalInput ) {
tsfCanonical.setInputElement( canonicalInput );
const state = JSON.parse( document.getElementById( `tsf-canonical-data_${canonicalId}` )?.dataset.state || 0 );
if ( state ) {
tsfCanonical.updateStateOf( canonicalId, 'allowReferenceChange', ! state.refCanonicalLocked );
tsfCanonical.updateStateOf( canonicalId, 'defaultCanonical', state.defaultCanonical.trim() );
tsfCanonical.updateStateOf( canonicalId, 'preferredScheme', state.preferredScheme.trim() );
tsfCanonical.updateStateOf( canonicalId, 'urlStructure', state.urlStructure );
}
tsfCanonical.enqueueTriggerUnregisteredInput( canonicalId );
document.body.addEventListener( 'tsf-canonical-scheme-changed', event => {
tsfCanonical.updateStateOf( canonicalId, 'preferredScheme', event.detail.scheme );
} );
}
const robotsData = {
site: new Map(),
pt: new Map(),
}
const isNo_Default = robotsType => {
let off = false;
if ( 'noindex' === robotsType )
off = ! _getPtaData()[ postType ].hasPosts;
return off || robotsData.site.get( robotsType ) || robotsData.pt.get( robotsType );
}
const updateRobots = robotsType => {
const robotsSelect = document.getElementById( _getPtaInputId( postType, robotsType ) );
if ( ! robotsSelect ) return;
const _defaultIndexOption = [ ...robotsSelect.options ].find( o => '0' === o.value ),
_data = robotsSelect.dataset || {};
const newHTML = _data.defaultI18n?.replace(
'%s',
tsf.decodeEntities(
isNo_Default( robotsType ) ? _data.defaultOff : _data.defaultOn,
)
);
if ( newHTML !== _defaultIndexOption?.innerHTML ) {
_defaultIndexOption.innerHTML = newHTML;
robotsSelect.dispatchEvent( new Event( 'change' ) );
}
}
const _registerPTDefaultRobotsValue = event => {
const { postType: pt, robotsType, set } = event.detail;
// Nothing to see here.
if ( postType !== pt ) return;
robotsData.pt.set( robotsType, set.has( postType ) );
updateRobots( robotsType );
}
const _registerSiteDefaultRobotsValue = event => {
const { checked, robotsType } = event.detail;
robotsData.site.set( robotsType, !! checked );
updateRobots( robotsType );
}
document.body.addEventListener( 'tsf-post-type-robots-changed', _registerPTDefaultRobotsValue );
document.body.addEventListener( 'tsf-site-robots-changed', _registerSiteDefaultRobotsValue );
[ 'noindex', 'nofollow', 'noarchive' ].forEach( type => {
updateRobots( type )
} );
/**
* @since 5.1.0
*
* @function
* @param {Number} value
*/
const setRobotsIndexingState = value => {
let type = '';
switch ( +value ) {
case 0: // default, unset since unknown.
type = isNo_Default( 'noindex' ) ? 'noindex' : 'index';
break;
case -1: // index
type = 'index';
break;
case 1: // noindex
type = 'noindex';
break;
}
if ( 'noindex' === type ) {
canonicalPhState |= BNOINDEX;
} else {
canonicalPhState &= ~BNOINDEX;
}
updateCanonicalPlaceholder();
}
indexSelect.addEventListener( 'change', event => setRobotsIndexingState( event.target.value ) );
setRobotsIndexingState( indexSelect.value );
}
/**
* Initializes PTA's main tab meta input listeners.
*
* @since 4.2.0
* @access private
*
* @param {String} postType The post type name.
*/
function _initPtaMainListeners( postType ) {
/**
* Enqueues meta title and description input triggers
*
* @function
*/
const enqueueGeneralInputListeners = () => {
tsfTitle.enqueueUnregisteredInputTrigger( _getPtaInputId( postType, 'doctitle' ) );
tsfDescription.enqueueUnregisteredInputTrigger( _getPtaInputId( postType, 'description' ) );
}
/**
* Enqueues doctitles input trigger synchronously on postbox collapse or open.
*
* @function
* @param {!jQuery.Event} event
* @param {Element} elem
*/
const triggerPostboxSynchronousUnregisteredInput = ( event, elem ) => {
if ( 'autodescription-post-type-archive-settings' === elem.id ) {
let inside = elem.querySelector( '.inside' );
if ( inside.offsetHeight > 0 && inside.offsetWidth > 0 ) {
enqueueGeneralInputListeners();
}
}
}
// jQuery: WP action.
$( document ).on( 'postbox-toggled', triggerPostboxSynchronousUnregisteredInput );
/**
* Enequeues doctitles and social input trigger synchronously on post type change.
* Triggers for the current post type only.
*
* @param {Event} event
*/
const triggerPtaSynchronousUnregisteredInput = event => {
if ( event.detail?.postType === postType ) {
// This also invokes inputs for the Social tabs, which is nice.
enqueueGeneralInputListeners();
}
}
document.body.addEventListener( 'tsf-post-type-archive-switched', triggerPtaSynchronousUnregisteredInput );
// This also triggers change for the homepage description, which isn't necessary. But, this trims down codebase.
document.getElementById( `tsf-post_type_archive_${postType}-tab-general` )
?.addEventListener( 'tsf-tab-toggled', enqueueGeneralInputListeners );
}
/**
* Initializes Social meta input.
*
* @since 4.1.0
* @access private
*/
function _initSocialSettings() {
const socialTitleRemoveAdditions = document.getElementById( _getSettingsId( 'social_title_rem_additions' ) );
/**
* Changes the useSocialTagline state for dynamic social-title-placeholder updates.
*
* @function
* @param {Event} event
*/
const updateSocialAdditions = event => {
if ( event.target.checked ) {
tsfSocial.updateStateAll( 'addAdditions', false );
} else {
tsfSocial.updateStateAll( 'addAdditions', true );
}
}
if ( socialTitleRemoveAdditions ) {
socialTitleRemoveAdditions.addEventListener( 'change', updateSocialAdditions );
_dispatchAtInteractive( socialTitleRemoveAdditions, 'change' );
}
const ogTagsToggle = document.getElementById( _getSettingsId( 'og_tags' ) );
/**
* Hides Open Graph fields if Open Graph is disabled.
*
* @function
* @param {Event} event
*/
const displayOgFields = event => {
document.getElementById( 'multi_og_image_wrapper' )
?.classList
.toggle( 'hidden', ! event.target.checked );
}
if ( ogTagsToggle ) {
ogTagsToggle.addEventListener( 'change', displayOgFields );
_dispatchAtInteractive( ogTagsToggle, 'change' );
}
/**
* Changes the tabs visibility and selectability during option toggles.
*
* @function
* @param {{id:string,tab:string}} toggleData
*/
const registerTagToggle = toggleData => {
if ( ! toggleData.id ) return;
const toggle = document.getElementById( _getSettingsId( toggleData.id ) );
/**
* @function
* @param {Event} event
*/
const hideDisableTab = event => {
tsfTabs.toggleTab( 'tsfSettings', `tsf-social-tab-${toggleData.tab}`, event.target.checked );
}
if ( toggle ) {
toggle.addEventListener( 'change', hideDisableTab );
_dispatchAtInteractive( toggle, 'change' );
}
};
[
{
id: 'og_tags', // option ID.
tab: 'postdates',
},
{
id: 'facebook_tags',
tab: 'facebook',
},
{
id: 'twitter_tags',
tab: 'twitter',
},
{
id: 'oembed_scripts',
tab: 'oembed',
},
].forEach( registerTagToggle );
const toggleCheckRegistry = new Set();
/**
* Changes the settings visibility and selectability during all option toggles.
* @function
* @param {Event} event
*/
const checkAllDisabled = event => {
if ( event.target.checked ) {
toggleCheckRegistry.add( event.target.name );
} else {
toggleCheckRegistry.delete( event.target.name );
}
document.getElementById( 'tsf-social-settings-wrapper' )
?.classList
.toggle( 'hidden', ! toggleCheckRegistry.size );
}
[ 'og_tags', 'facebook_tags', 'twitter_tags', 'oembed_scripts' ].forEach( id => {
const toggle = document.getElementById( _getSettingsId( id ) );
toggle.addEventListener( 'change', checkAllDisabled );
_dispatchAtInteractive( toggle, 'change' );
} );
/**
* Emits hooks for when the Twitter Card is toggled.
* @function
* @param {Event} event
*/
const dispatchCardToggleEvent = event => {
document.body.dispatchEvent( new CustomEvent(
'tsf-update-twitter-card-type',
{
detail: {
cardType: event.target.value,
}
}
) );
}
document.querySelectorAll( '#tsf-twitter-cards input' ).forEach( el => {
el.addEventListener( 'change', dispatchCardToggleEvent );
if ( el.checked )
_dispatchAtInteractive( el, 'change' );
} );
}
/**
* Initializes Schema settings inputs.
*
* @since 5.0.0
* @access private
*/
function _initSchemaSettings() {
const sdToggle = document.getElementById( _getSettingsId( 'ld_json_enabled' ) );
/**
* @function
* @param {Event} event
*/
const toggleSettingsDisplay = event => {
document.getElementById( 'tsf-advanced-structured-data-settings-wrapper' )
?.classList
.toggle( 'hidden', ! event.target.checked );
togglePresenceTab();
}
if ( sdToggle ) {
sdToggle.addEventListener( 'change', toggleSettingsDisplay );
_dispatchAtInteractive( sdToggle, 'change' );
}
const presenceTab = {
id: 'knowledge_output', // option ID.
tab: 'presence',
};
const presenceToggle = document.getElementById( _getSettingsId( presenceTab.id ) ),
presenceTabRadio = document.getElementById( `tsf-social-tab-${presenceTab.tab}` );
const presenceTabLabel = document.getElementById( 'schema-tabs-wrapper' )
?.querySelector( `[for=tsf-schema-tab-${presenceTab.tab}]` );
/**
* @function
*/
const togglePresenceTab = () => {
let show = sdToggle?.checked && presenceToggle?.checked;
presenceTabLabel?.classList.toggle( 'hidden', ! show );
show
? presenceTabRadio?.removeAttribute( 'disabled' )
: presenceTabRadio?.setAttribute( 'disabled', '' );
}
if ( presenceToggle ) {
presenceToggle.addEventListener( 'change', togglePresenceTab );
togglePresenceTab( presenceToggle, 'change' );
}
const knowledgeTypeSelect = document.getElementById( _getSettingsId( 'knowledge_type' ) );
/**
* @function
* @param {Event} event
*/
const toggleKnowledgeType = event => {
document.getElementById( 'tsf-logo-structured-data-settings-wrapper' )
?.classList
.toggle( 'hidden', event.target.value === 'person' );
}
if ( knowledgeTypeSelect ) {
knowledgeTypeSelect.addEventListener( 'change', toggleKnowledgeType );
_dispatchAtInteractive( knowledgeTypeSelect, 'change' );
}
const logoToggle = document.getElementById( _getSettingsId( 'knowledge_logo' ) );
/**
* @function
* @param {Event} event
*/
const toggleDisplayLogo = event => {
document.getElementById( 'tsf-logo-upload-structured-data-settings-wrapper' )
?.classList
.toggle( 'hidden', ! event.target.checked );
}
if ( logoToggle ) {
logoToggle.addEventListener( 'change', toggleDisplayLogo );
_dispatchAtInteractive( logoToggle, 'change' );
}
}
/**
* Initializes Robots' meta input.
*
* @since 4.0.2
* @since 4.1.1 Now adds taxonomy warnings.
* @access private
*/
function _initRobotsInputs() {
const copyrightToggle = document.getElementById( _getSettingsId( 'set_copyright_directives' ) );
if ( copyrightToggle ) {
const controlNodes = [
"max_snippet_length",
"max_image_preview",
"max_video_preview",
].map( name => document.getElementById( _getSettingsId( name ) ) );
const surrogateClass = 'tsf-toggle-directives-surrogate';
/**
* Toggles copyright directive option states.
*
* @function
* @param {Event} event
*/
const toggleCopyrightControl = event => {
if ( event.target.checked ) {
controlNodes.forEach( el => el.disabled = false );
document.querySelectorAll( `.${surrogateClass}` ).forEach( el => el.remove() );
} else {
controlNodes.forEach( el => {
el.disabled = true;
let surrogate = document.createElement( 'input' );
surrogate.type = 'hidden';
surrogate.name = el.name || '';
surrogate.value = el.value || 0;
surrogate.classList.add( surrogateClass );
el.insertAdjacentElement( 'afterend', surrogate );
} );
}
}
copyrightToggle.addEventListener( 'change', toggleCopyrightControl );
_dispatchAtInteractive( copyrightToggle, 'change' );
}
const robotsPostTypes = {},
robotsPtTaxonomies = {};
[ robotsPostTypes, robotsPtTaxonomies ].forEach( _const => {
_const.noindex = new Set();
_const.nofollow = new Set();
_const.noarchive = new Set();
} );
const dispatchPosttypeRobotsChangedEvent = ( postType, robotsType ) => {
document.body.dispatchEvent( new CustomEvent(
'tsf-post-type-robots-changed',
{
detail: {
postType,
robotsType,
set: robotsPostTypes[ robotsType ],
}
}
) );
}
const dispatchTaxonomyRobotsChangedEvent = ( taxonomy, robotsType ) => {
document.body.dispatchEvent( new CustomEvent(
'tsf-taxonomy-robots-changed',
{
detail: {
taxonomy,
robotsType,
set: robotsPtTaxonomies[ robotsType ],
}
}
) );
}
const dispatchSiteRobotsChangedEvent = ( checked, robotsType ) => {
document.body.dispatchEvent( new CustomEvent(
'tsf-site-robots-changed',
{
detail: {
checked,
robotsType,
}
}
) );
}
const postTypeRobotsHelp = wp.template( 'tsf-robots-pt-help' )();
const addTaxRobotsByPtWarning = ( taxonomy, robotsType, disable ) => {
// Yes, stacked template literals. Sue me :)
let taxEl = document.getElementById( `${ _getSettingsId( `${robotsType}_taxonomies` ) }[${taxonomy}]` );
if ( disable ) {
taxEl.closest( 'label' ).insertAdjacentHTML( 'beforeend', postTypeRobotsHelp );
tsfTT.triggerReset();
} else {
// 'tsf-taxonomy-from-pt-robots-warning' is defined at `../inc/views/templates/settings/settings.php`
taxEl.closest( 'label' ).querySelector( '.tsf-taxonomy-from-pt-robots-warning' )?.remove();
}
toggleWarnings( taxonomy );
}
const validateTaxonomyState = robotsType => {
// We want to show that the taxonomy is de-robotsTyped, but make that auto-reversible, and somehow still enactable?
const taxEntries = document.querySelectorAll( `.tsf-robots-taxonomies[data-robots="${robotsType}"]` );
let triggerchange = false;
taxEntries.forEach( element => {
// get taxonomy from last [] entry.
let taxonomy = element.name.split( /(?:.+\[)(.+?)(?:])/ ).join( '' );
const taxPostTypes = JSON.parse( element.dataset.postTypes || 0 ),
hasRobots = taxPostTypes && taxPostTypes.every( postType => robotsPostTypes[ robotsType ].has( postType ) );
if ( hasRobots ) {
if ( ! robotsPtTaxonomies[ robotsType ].has( taxonomy ) ) {
// Newly disabled, trigger change.
triggerchange = true;
}
// Filter it out to prevent duplicates. Redundant?
robotsPtTaxonomies[ robotsType ].add( taxonomy );
} else {
if ( robotsPtTaxonomies[ robotsType ].has( taxonomy ) ) {
robotsPtTaxonomies[ robotsType ].delete( taxonomy );
// Enabled again, was disabled. Trigger change.
triggerchange = true;
}
}
// TODO Collect and combine changes, to condense paint stack (perceptive performance, reduce race condition changes)?
triggerchange && dispatchTaxonomyRobotsChangedEvent( taxonomy, robotsType );
} );
}
const validateTaxonomiesCache = {
noindex: new Map(),
nofollow: new Map(),
noarchive: new Map(),
};
const getValidateTaxonomiesCache = ( key, robotsType ) => validateTaxonomiesCache[ robotsType ].get( key ) || ( new Set() );
// TODO trigger new events here, to make it easier to work with for others?
const validateTaxonomies = event => {
const { taxonomy, robotsType } = event.detail;
if ( getValidateTaxonomiesCache( 'robotsPtTaxonomies', robotsType ).size
!== robotsPtTaxonomies[ robotsType ].size
) addTaxRobotsByPtWarning( taxonomy, robotsType, robotsPtTaxonomies[ robotsType ].has( taxonomy ) );
// Create new pointers in the memory by shadowcloning the object.
validateTaxonomiesCache[ robotsType ].set( 'robotsPtTaxonomies', new Set( robotsPtTaxonomies[ robotsType ] ) );
}
document.body.addEventListener( 'tsf-taxonomy-robots-changed', validateTaxonomies );
const validatePostTypes = event => {
validateTaxonomyState( event.detail.robotsType );
}
document.body.addEventListener( 'tsf-post-type-robots-changed', validatePostTypes );
/**
* Add exclusions support by removing duplicated warnings.
* @param {string} taxonomy
*/
const toggleWarnings = taxonomy => {
for ( let robotsType in robotsPtTaxonomies ) {
if ( robotsPtTaxonomies[ robotsType ].has( taxonomy ) ) {
let taxEl = document.getElementById( `${ _getSettingsId( `${robotsType}_taxonomies` ) }[${taxonomy}]` ),
warning = taxEl.closest( 'label' ).querySelector( '.tsf-taxonomy-from-pt-robots-warning' );
if ( taxEl.dataset.disabledWarning ) {
warning.style.display = 'none';
} else {
warning.style.display = '';
}
}
}
}
document.body.addEventListener( 'tsf-taxonomy-support-changed', event => toggleWarnings( event.detail.taxonomy ) );
// This prevents notice-removal checks before they're added.
let init = false;
const checkRobotsPT = event => {
// get post type from last [] entry.
let postType = event.target?.name.split( /(?:.+\[)(.+?)(?:])/ ).join( '' ),
robotsType = event.target?.dataset.robots;
if ( event.target.checked ) {
robotsPostTypes[ robotsType ].add( postType );
dispatchPosttypeRobotsChangedEvent( postType, robotsType );
} else {
// No need to filter when it was never registered in the first place.
if ( init ) {
robotsPostTypes[ robotsType ].delete( postType );
dispatchPosttypeRobotsChangedEvent( postType, robotsType );
}
}
}
document.querySelectorAll( '.tsf-robots-post-types' ).forEach( el => {
el.addEventListener( 'change', checkRobotsPT );
_dispatchAtInteractive( el, 'change' );
} );
const checkRobotsSite = event => {
let robotsType = event.target?.dataset.robots,
checked = event.target.checked;
if ( checked ) {
dispatchSiteRobotsChangedEvent( checked, robotsType );
} else {
// Dispatch only when something new is introduced.
if ( init ) {
dispatchSiteRobotsChangedEvent( checked, robotsType );
}
}
}
document.querySelectorAll( '.tsf-robots-site' ).forEach( el => {
el.addEventListener( 'change', checkRobotsSite );
_dispatchAtInteractive( el, 'change' );
} );
init = true;
}
/**
* Initializes robots Post Type support.
*
* @since 4.2.0
* @access private
*/
function _initRobotsSupport() {
/**
* @param {string} postType
* @return {string} The cloned input class used for sending POST data.
*/
const getCloneClassPT = postType => tsf.escapeString( `tsf-disabled-post-type-input-clone-${postType}` );
const postTypeHelpTemplate = wp.template( 'tsf-disabled-post-type-help' )();
/**
* @param {string} postType
* @return {array} A list of affected post type settings.
*/
const getPostTypeRobotsSettings = postType => [
document.getElementById( `${ _getSettingsId( 'noindex_post_types' ) }[${postType}]` ),
document.getElementById( `${ _getSettingsId( 'nofollow_post_types' ) }[${postType}]` ),
document.getElementById( `${ _getSettingsId( 'noarchive_post_types' ) }[${postType}]` ),
].filter( el => el );
const augmentPTRobots = event => {
const { postType, set } = event.detail;
if ( set.has( postType ) ) {
getPostTypeRobotsSettings( postType ).forEach( element => {
if ( ! element ) return;
let clone = element.cloneNode( true );
clone.type = 'hidden';
// Because the clone is hidden, we must set its value based on the checked state's + value thereof:
clone.value = element.checked ? element.value : '';
// Note that this might cause inconsistencies when other JS elements try to amend the data via ID.
// However, they should use 'getElementsByName', anyway.
clone.id += '-cloned';
clone.classList.add( getCloneClassPT( postType ) );
element.disabled = true;
element.dataset.disabledWarning = 1;
const label = element.closest( 'label' );
label.insertAdjacentHTML( 'beforeend', postTypeHelpTemplate );
label.append( clone );
} );
tsfTT.triggerReset();
} else {
getPostTypeRobotsSettings( postType ).forEach( element => {
if ( ! element ) return;
if ( ! element.dataset.disabledWarning ) return;
// 'tsf-post-type-warning' is defined at `../inc/views/templates/settings/settings.php`
element.closest( 'label' ).querySelector( '.tsf-post-type-warning' ).remove();
document.querySelectorAll( `.${getCloneClassPT( postType )}` ).forEach(
clone => { clone.remove() }
);
element.disabled = false;
element.dataset.disabledWarning = '';
} );
}
}
document.body.addEventListener( 'tsf-post-type-support-changed', augmentPTRobots );
const taxonomyHelpTemplate = wp.template( 'tsf-disabled-taxonomy-help' )();
const taxonomyPtHelpTemplate = wp.template( 'tsf-disabled-taxonomy-from-pt-help' )();
/**
* @param {string} taxonomy
* @return {string} The cloned input class used for sending POST data.
*/
const getCloneClassTaxonomy = taxonomy => tsf.escapeString( `tsf-disabled-taxonomy-input-clone-${taxonomy}` );
/**
* @param {string} taxonomy
* @return {array} A list of affected post type settings.
*/
const getTaxonomyRobotsSettings = taxonomy => [
document.getElementById( `${ _getSettingsId( 'noindex_taxonomies' ) }[${taxonomy}]` ),
document.getElementById( `${ _getSettingsId( 'nofollow_taxonomies' ) }[${taxonomy}]` ),
document.getElementById( `${ _getSettingsId( 'noarchive_taxonomies' ) }[${taxonomy}]` ),
].filter( el => el );
const augmentTaxonomyRobots = event => {
const { taxonomy, set, setPt, setAll } = event.detail;
if ( setAll.has( taxonomy ) ) {
getTaxonomyRobotsSettings( taxonomy ).forEach( element => {
if ( ! element ) return;
let clone = element.cloneNode( true );
clone.type = 'hidden';
// Because the clone is hidden, we must set its value based on the checked state's + value thereof:
clone.value = element.checked ? element.value : '';
// Note that this might cause inconsistencies when other JS elements try to amend the data via ID.
// However, they should use 'getElementsByName', anyway.
clone.id += '-cloned';
clone.classList.add( getCloneClassTaxonomy( taxonomy ) );
element.disabled = true;
element.dataset.disabledWarning = 1;
const label = element.closest( 'label' );
// 'tsf-taxonomy-warning' is defined at `../inc/views/templates/settings/settings.php`
if ( ! label.querySelector( '.tsf-taxonomy-warning' ) )
label.insertAdjacentHTML( 'beforeend', taxonomyHelpTemplate );
if ( ! label.querySelector( getCloneClassTaxonomy( taxonomy ) ) )
label.append( clone );
} );
tsfTT.triggerReset();
} else {
getTaxonomyRobotsSettings( taxonomy ).forEach( element => {
if ( ! element ) return;
if ( ! element.dataset.disabledWarning ) return;
// 'tsf-taxonomy-warning' is defined at `../inc/views/templates/settings/settings.php`
element.closest( 'label' ).querySelector( '.tsf-taxonomy-warning' )?.remove();
document.querySelectorAll( `.${getCloneClassTaxonomy( taxonomy )}` ).forEach(
clone => { clone.remove() }
);
element.disabled = false;
element.dataset.disabledWarning = '';
} );
}
const taxEl = document.getElementById( `${ _getSettingsId( 'disabled_taxonomies' ) }[${taxonomy}]` );
if ( setPt.has( taxonomy ) ) {
// 'tsf-taxonomy-from-pt-warning' is defined at `../inc/views/templates/settings/settings.php`
if ( ! taxEl.closest( 'label' ).querySelector( '.tsf-taxonomy-from-pt-warning' ) ) {
taxEl.closest( 'label' ).insertAdjacentHTML( 'beforeend', taxonomyPtHelpTemplate );
tsfTT.triggerReset();
}
} else {
// 'tsf-taxonomy-from-pt-warning' is defined at `../inc/views/templates/settings/settings.php`
taxEl.closest( 'label' ).querySelector( '.tsf-taxonomy-from-pt-warning' )?.remove();
}
}
document.body.addEventListener( 'tsf-taxonomy-support-changed', augmentTaxonomyRobots );
}
/**
* Initializes Webmasters' meta input.
*
* @since 4.0.0
* @access private
*/
function _initWebmastersInputs() {
const webmasterNodes = [
"google_verification",
"bing_verification",
"yandex_verification",
"baidu_verification",
"pint_verification",
].map( name => document.getElementById( _getSettingsId( name ) ) );
/**
* @function
* @param {Event} event
*/
const trimScript = event => {
let val = event.clipboardData && event.clipboardData.getData( 'text' ) || '';
if ( val ) {
// Extrude tag paste's content value and set that as a value.
let match = /<meta\b[^>]+?\bcontent=(["'])?([^"'>\s]+)\1?[^>]*?>/i.exec( val );
if ( match?.[2]?.length ) {
event.stopPropagation();
event.preventDefault(); // Prevents save listener.. TODO why?
event.target.value = match[2];
// Tell change:
tsfAys.registerChange();
}
}
}
webmasterNodes.forEach( el => el.addEventListener( 'paste', trimScript ) );
}
/**
* Initializes Sitemap input.
*
* @since 5.0.5
* @access private
*/
function _initSitemapInputs() {
const optimizedSitemapsToggle = document.getElementById( _getSettingsId( 'sitemaps_output' ) ),
cacheSitemapsToggle = document.getElementById( _getSettingsId( 'cache_sitemap' ) );
/**
* @function
* @param {Event} event
*/
const updateSocialAdditions = event => {
const sitemapsEnabled = !! event.target.checked;
tsfTabs.toggleTab( 'tsfSettings', 'tsf-sitemaps-tab-style', sitemapsEnabled );
document.getElementById( 'tsf-sitemap-transient-cache-settings' )
?.classList.toggle( 'hidden', ! sitemapsEnabled );
}
if ( optimizedSitemapsToggle ) {
optimizedSitemapsToggle.addEventListener( 'change', updateSocialAdditions );
_dispatchAtInteractive( optimizedSitemapsToggle, 'change' );
}
const toggleCheckRegistry = new Map();
/**
* Changes the settings visibility and selectability during all option toggles.
* @function
* @param {Event} event
*/
const checkAllEnabled = event => {
const prerenderingSettings = document.getElementById( 'tsf-sitemap-prerendering-settings' );
toggleCheckRegistry.set( event.target.name, !! event.target.checked );
for ( const val of toggleCheckRegistry.values() ) {
if ( ! val ) {
prerenderingSettings?.classList.add( 'hidden', ! toggleCheckRegistry.size );
return;
}
}
prerenderingSettings?.classList.remove( 'hidden' );
}
// optimizedSitemapsToggle is a master toggle here -- though, since there's only two... shrug.
[ optimizedSitemapsToggle, cacheSitemapsToggle ].forEach( toggle => {
if ( toggle ) {
toggle.addEventListener( 'change', checkAllEnabled );
_dispatchAtInteractive( toggle, 'change' );
}
} );
}
/**
* Initializes settings scripts on TSF-load.
*
* @since 4.0.0
* @since 5.1.0 Added error handling.
* @access private
*/
function _loadSettings() {
// One is not reliant on the other; this way, if one crashes, the rest still works.
[
_initSubmit,
_initGeneralSettings,
_initTitleSettings,
_initHomeGeneralListeners,
_initHomeTitleSettings,
_initHomeDescriptionSettings,
_initHomeSocialSettings,
_initHomeVisibilitySettings,
_initPtaSettings,
_initSocialSettings,
_initSchemaSettings,
_initRobotsInputs,
_initRobotsSupport,
_initWebmastersInputs,
_initSitemapInputs,
_initColorPicker,
].forEach( fn => {
try {
fn();
} catch ( error ) {
console.error( `Error in ${fn.name}:`, error );
}
} );
}
/**
* Sets a class to the active element which helps excluding focus rings.
*
* @since 4.0.0
* @since 4.1.3 Now offloaded to tsfTabs.
* @access private
*/
function _initTabs() {
tsfTabs.initStack(
'tsfSettings',
{
tabToggledEvent: new CustomEvent( 'tsf-tab-toggled' ),
HTMLClasses: {
wrapper: 'tsf-nav-tab-wrapper',
tabRadio: 'tsf-nav-tab-radio',
tabLabel: 'tsf-nav-tab-label',
activeTab: 'tsf-nav-tab-active',
activeTabContent: 'tsf-nav-tab-content-active',
},
fixHistory: true, // false for flex? Doesn't seem like it was?
}
);
}
return Object.assign( {
/**
* Initialises all aspects of the scripts.
* You shouldn't call this.
*
* @since 4.0.0
* @since 4.0.3 Now also displaces notice-info.
* @access protected
*
* @function
*/
load: () => {
// Execute this ASAP, to prevent late layout shifting. Use same anchor as core--so to prevent subsequent movement.
const headerEnd = document.querySelector( '.wp-header-end' );
document.querySelectorAll(
'div.updated, div.error, div.notice, .notice-error, .notice-warning, .notice-info'
).forEach( el => { headerEnd.insertAdjacentElement( 'afterend', el ) } );
document.body.addEventListener( 'tsf-onload', _loadSettings );
// Initializes tabs early; we rely a fallback event that tsf-onload/tsf-ready uses there.
_initTabs();
},
}, {
l10n,
} );
}( jQuery );
window.tsfSettings.load();