%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /var/www/pn/beta/64801_wp-content/plugins/autodescription/lib/js/
Upload File :
Create Path :
Current File : /var/www/pn/beta/64801_wp-content/plugins/autodescription/lib/js/tsf.js

/**
 * This file holds The SEO Framework plugin's JS code.
 * Serve JavaScript as an addition, not as a means.
 *
 * @author Sybre Waaijer <https://cyberwire.nl/>
 * @link https://wordpress.org/plugins/autodescription/
 */

/**
 * The SEO Framework plugin
 * Copyright (C) 2015 - 2018 Sybre Waaijer, CyberWire (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/>.
 */

// ==ClosureCompiler==
// @compilation_level ADVANCED_OPTIMIZATIONS
// @output_file_name tsf.min.js
// @externs_url https://raw.githubusercontent.com/google/closure-compiler/master/contrib/externs/jquery-1.9.js
// @externs_url https://raw.githubusercontent.com/sybrew/the-seo-framework/master/lib/js/externs/tsf.externs.js
// ==/ClosureCompiler==
// http://closure-compiler.appspot.com/home

'use strict';

/**
 * Holds The SEO Framework values in an object to avoid polluting global namespace.
 *
 * @since 2.2.4
 * @todo spread methods accross files/classes through protoype?
 *
 * @constructor
 */
window.tsf = {

	/**
	 * AJAX Nonces object.
	 *
	 * @since 2.9.0
	 *
	 * @type {Object<string, string>} nonces The AJAX nonces
	 */
	nonces : tsfL10n.nonces,

	/**
	 * i18n object.
	 *
	 * @since 2.8.0
	 *
	 * @const
	 * @type {Object<string, string>} i18n Localized strings
	 */
	i18n : tsfL10n.i18n,

	/**
	 * Page states object.
	 *
	 * @since 2.8.0
	 *
	 * @const
	 * @type {Object<string, *>} states Localized states
	 */
	states : tsfL10n.states,

	/**
	 * Option parameters object.
	 *
	 * @since 2.8.0
	 *
	 * @const
	 * @type {Object<string, *>} params Localized parameters
	 */
	params : tsfL10n.params,

	/**
	 * Other parameters object.
	 *
	 * @since 2.8.0
	 *
	 * @const
	 * @type {Object<string, ?>} other Localized strings|parameters|states
	 */
	other : tsfL10n.other,

	/**
	 * Determines if the settings have been changed since visit.
	 *
	 * @since 2.2.0
	 *
	 * @typedef {(Boolean|null|undefined)} settingsChanged
	 */
	settingsChanged: false,

	/**
	 * Mixed string and int (i10n is string, JS is int).
	 *
	 * @since 2.6.0
	 *
	 * @type {(String|number)} countertype The counterType
	 */
	counterType : 0,

	/**
	 * Determines if the current page has input boxes for The SEO Framework.
	 *
	 * @since 2.7.0
	 *
	 * @typedef {(Boolean|null|undefined)} hasInput
	 */
	hasInput : false,

	/**
	 * The current character counter additions class.
	 *
	 * @since 2.6.0
	 *
	 * @type {(string|null)} additionsClass
	 */
	additionsClass : '',

	/**
	 * Image cropper instance.
	 *
	 * @since 2.8.0
	 *
	 * @type {!Object} cropper
	 */
	cropper : {},

	/**
	 * Helper function for confirming a user action.
	 *
	 * @since 2.2.4
	 *
	 * @function
	 * @param {String} text The text to display.
	 * @return {(Boolean|null)} True on OK, false on cancel.
	 */
	confirm: function( text ) {
		return confirm( text );
	},

	/**
	 * Escapes input string.
	 *
	 * @since 3.0.1
	 *
	 * @source <https://stackoverflow.com/a/4835406>
	 * @function
	 * @param {string} str
	 * @return {string}
	 */
	escapeString: function( str ) {

		if ( ! str.length )
			return '';

		let map = {
			'&': '&amp;',
			'<': '&lt;',
			'>': '&gt;',
			'"': '&quot;',
			"'": '&#039;'
		};

		return str.replace( /[&<>"']/g, m => {
			return map[ m ];
		} );
	},

	/**
	 * Undoes what tsf.escapeString has done.
	 *
	 * @since 3.0.4
	 *
	 * @function
	 * @param {string} str The escaped str via tsf.escapeString
	 * @return {string}
	 */
	unescapeString: function( str ) {

		if ( ! str.length )
			return '';

		let map = {
			'&': '&amp;',
			'<': '&lt;',
			'>': '&gt;',
			'"': '&quot;',
			"'": '&#039;'
		};

		//= IE11 replacement for Object.values. <https://stackoverflow.com/a/42830295>
		let values = Object.keys( map ).map( e => map[ e ] );

		let regex = new RegExp(
			values.map( v => {
				return v.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' );
			} ).join('|'),
			'g'
		);

		return str.replace( regex, m => {
			return Object.keys( map ).find( k => {
				return map[ k ] === m;
			} );
		} );
	},

	/**
	 * Gets string length.
	 * We do not trim whitespace in JavaScript; that should be self-taught by the user.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @param {string} str
	 * @return {number}
	 */
	getStringLength: function( str ) {
		let e,
			length = 0;

		if ( str.length ) {
			e = document.createElement( 'span' );
			e.innerHTML = tsf.escapeString( str );
			length = e.childNodes[0].nodeValue.length;
		}
		return +length;
	},

	/**
	 * Updates pixel counter.
	 *
	 * @since 3.0.0
	 * @access private
	 *
	 * @function
	 * @param {Object} test
	 * @return {undefined}
	 */
	updatePixelCounter: function( test ) {

		let el = test.e,
			text = test.text,
			guideline = test.guideline;

		let wrap = el.parentElement;

		if ( ! wrap )
			return;

		let bar = wrap.querySelector( '.tsf-pixel-counter-bar' ),
			shadow = wrap.querySelector( '.tsf-pixel-counter-shadow' );

		if ( ! bar || ! shadow )
			return;

		shadow.innerHTML = tsf.escapeString( text );

		let testWidth = shadow.offsetWidth,
			newClass = '',
			newWidth = '';

		let fitClass = 'tsf-pixel-counter-fit',
			overflownClass = 'tsf-pixel-counter-overflown';

		if ( testWidth > guideline ) {
			//= Can never be 0. Good. Add 2/3rds of difference to it; implying emphasis.
			newWidth = ( guideline / ( testWidth + ( ( testWidth - guideline ) * 2 / 3 ) ) * 100 ) + '%';
			newClass = overflownClass;
		} else {
			//= Can never be over 100. Good.
			newWidth = ( testWidth / guideline * 100 ) + '%';
			newClass = fitClass;
		}

		let sub = bar.querySelector( '.tsf-pixel-counter-fluid' ),
			label;

		label = tsf.i18n.pixelsUsed.replace( /%1\$d/g, testWidth );
		label = label.replace( /%2\$d/g, guideline );

		bar.classList.remove( fitClass, overflownClass );
		bar.classList.add( newClass );
		bar.dataset.desc = label;
		bar.setAttribute( 'aria-label', label );

		sub.style.width = newWidth;

		tsf._triggerTooltipUpdate( bar );
	},

	/**
	 * Initializes all aspects for title editing.
	 * Assumes only one title editor is present in the DOM.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {undefined}
	 */
	_initTitleInputs: function() {

		if ( ! tsf.hasInput )
			return;

		let $doctitles = jQuery( "#autodescription_title, #autodescription-meta\\[doctitle\\], #autodescription-site-settings\\[homepage_title\\]" );

		if ( ! $doctitles.length )
			return;

		//= y u no fix dis, Microsoft. Crappy vars don't deserve CamelCase.
		let ie11killswitch = false,
			ie11 = !! navigator.userAgent.match(/Trident\/7\./);

		let hoverPrefixPlacement,
			hoverAdditionsPlacement,
			hoverPrefixElement,
			hoverPrefixValue = '',
			hoverAdditionsElement,
			hoverAdditionsValue = '',
			separator = tsf.params.titleSeparator,
			defaultTitle = tsf.params.defaultTitle;

		let useTagline = tsf.states.useTagline,
			isRTL = tsf.states.isRTL,
			isPrivate = tsf.states.isPrivate,
			isPasswordProtected = tsf.states.isPasswordProtected;

		//= Sets hoverPrefixPlacement.
		hoverPrefixPlacement = isRTL ? 'after' : 'before';
		//= Sets hoverAdditionsPlacement.
		const setHoverAdditionsPlacement = function() {
			let placement = 'before';

			if ( tsf.states.isSettingsPage ) {
				if ( isRTL ) {
					if ( 'right' === jQuery( '#tsf-home-title-location input:checked' ).val() ) {
						placement = 'after';
					}
				} else {
					if ( 'left' === jQuery( '#tsf-home-title-location input:checked' ).val() ) {
						placement = 'after';
					}
				}
			} else {
				if ( tsf.states.isHome ) {
					//= Static front page.
					if ( isRTL ) {
						if ( 'right' === tsf.params.titleLocation )
							placement = 'after';
					} else if ( 'left' === tsf.params.titleLocation ) {
						placement = 'after';
					}
				} else {
					if ( isRTL ) {
						if ( 'left' === tsf.params.titleLocation )
							placement = 'after';
					} else if ( 'right' === tsf.params.titleLocation ) {
						placement = 'after';
					}
				}
			}

			hoverAdditionsPlacement = placement;
		}
		setHoverAdditionsPlacement();
		//= Sets hoverAdditionsValue.
		const setHoverAdditionsValue = function() {
			let newValue = '';

			if ( tsf.states.isSettingsPage ) {
				if ( useTagline ) {
					//= Tagline is enabled.
					let e = document.getElementById( 'autodescription-site-settings[homepage_title_tagline]' ),
						customTagline = e ? e.value : '';

					if ( customTagline.length ) {
						newValue = customTagline;
					} else {
						newValue = tsf.params.blogDescription;
					}
				}
			} else if ( tsf.states.isHome ) {
				//= Static front page.
				if ( useTagline )
					newValue = tsf.params.titleAdditions;
			} else {
				//= Global additions are enabled.
				if ( useTagline )
					newValue = tsf.params.titleAdditions;
			}

			if ( newValue.length ) {
				newValue = tsf.escapeString( newValue );
				switch ( hoverAdditionsPlacement ) {
					case 'before' :
						newValue = newValue + ' ' + separator + ' ';
						break;

					case 'after' :
						newValue = ' ' + separator + ' ' + newValue;
						break;
				}
			}
			hoverAdditionsValue = newValue.length ? newValue : '';

			hoverAdditionsElement = document.getElementById( 'tsf-title-placeholder' );
			if ( hoverAdditionsValue.length && hoverAdditionsElement ) {
				hoverAdditionsElement.innerHTML = hoverAdditionsValue;
			}
		}
		setHoverAdditionsValue();
		//= Sets hoverPrefixValue.
		const setHoverPrefixValue = function() {
			let newValue = '';

			if ( isPrivate ) {
				newValue = tsf.i18n.privateTitle;
			} else if ( isPasswordProtected ) {
				newValue = tsf.i18n.protectedTitle;
			}

			if ( newValue.length ) {
				newValue = tsf.escapeString( newValue );
				switch ( hoverPrefixPlacement ) {
					case 'before' :
						newValue = newValue + ' ';
						break;

					case 'after' :
						newValue = ' ' + newValue;
						break;
				}
			}
			hoverPrefixValue = newValue.length ? newValue : '';

			if ( hoverPrefixValue.length && hoverPrefixElement ) {
				hoverPrefixElement.innerHTML = hoverPrefixValue;
			}

			hoverPrefixElement = document.getElementById( 'tsf-title-placeholder-prefix' );
			if ( hoverPrefixValue.length && hoverPrefixElement ) {
				hoverPrefixElement.innerHTML = hoverPrefixValue;
			}
		}
		setHoverPrefixValue();
		const updateHoverPlacement = function( event ) {

			if ( ! hoverAdditionsElement && ! hoverPrefixElement )
				return;

			let $input = jQuery( event.target ),
				inputValue = $input.val();

			let hasAdditionsValue = !! hoverAdditionsValue.length,
				hasPrefixValue = !! hoverPrefixValue.length;

			if ( ! hasAdditionsValue && hoverAdditionsElement )
				hoverAdditionsElement.style.display = 'none';
			if ( ! hasPrefixValue && hoverPrefixElement )
				hoverPrefixElement.style.display = 'none';

			if ( ! hasAdditionsValue && ! hasPrefixValue ) {
				//= Both items are emptied through settings.
				$input.css( 'text-indent', 'initial' );
				return;
			}

			if ( ! inputValue.length ) {
				//= Input is emptied.
				$input.css( 'text-indent', "initial" );
				if ( hoverAdditionsElement ) hoverAdditionsElement.style.display = 'none';
				if ( hoverPrefixElement ) hoverPrefixElement.style.display = 'none';
				return;
			}

			let outerWidth = $input.outerWidth( true ),
				verticalPadding = ( $input.outerHeight( true ) - $input.height() ) / 2,
				horizontalPadding = ( outerWidth - $input.innerWidth() ) / 2;

			let offsetPosition = isRTL ? 'right' : 'left',
				leftOffset = ( $input.outerWidth( true ) - $input.width() ) / 2;

			let fontStyleCSS = {
				'display': $input.css( "display" ),
				'lineHeight': $input.css( "lineHeight" ),
				'fontFamily': $input.css( "fontFamily" ),
				'fontWeight': $input.css( "fontWeight" ),
				'fontSize': $input.css( "fontSize" ),
				'letterSpacing': $input.css( "letterSpacing" ),
				'paddingTop': verticalPadding + "px",
				'paddingBottom': verticalPadding + "px",
			};

			let $prefixElement = jQuery( hoverPrefixElement ),
				$additionsElement = jQuery( hoverAdditionsElement );

			let additionsMaxWidth = 0,
				additionsOffset = 0,
				prefixOffset = 0,
				totalIndent = 0,
				prefixMaxWidth = 0;

			let elipsisWidth = 0; // TODO make this real? x-Browser incompatible!

			if ( hasPrefixValue ) {
				$prefixElement.css( fontStyleCSS );
				$prefixElement.css( { 'maxWidth' : 'initial' } );
				prefixMaxWidth = $prefixElement[0].getBoundingClientRect().width;
				if ( prefixMaxWidth < elipsisWidth )
					prefixMaxWidth = 0;
			}
			if ( hasAdditionsValue ) {
				let textWidth = 0;

				(function() {
					let $offsetTest = jQuery( "#tsf-title-offset" );
					$offsetTest.text( inputValue );
					$offsetTest.css({
						'fontFamily' : fontStyleCSS.fontFamily,
						'fontWeight' : fontStyleCSS.fontWeight,
						'letterSpacing' : fontStyleCSS.letterSpacing,
						'fontSize' : fontStyleCSS.fontSize,
					});
					textWidth = $offsetTest[0].getBoundingClientRect().width;
				})();

				//= Input element width - Padding - input text width - prefix value width.
				additionsMaxWidth = $input[0].getBoundingClientRect().width - horizontalPadding - leftOffset - textWidth - prefixMaxWidth;
				if ( additionsMaxWidth < elipsisWidth ) {
					//= Add width to the prefix element, so it may stay its size, and hide the additions.
					prefixMaxWidth += additionsMaxWidth;
					additionsMaxWidth = 0;
				}
				$additionsElement.css( fontStyleCSS );
				$additionsElement.css( { 'maxWidth' : 'initial' } );

				switch ( hoverAdditionsPlacement ) {
					case 'before' :
						let additionsWidth = $additionsElement[0].getBoundingClientRect().width;

						additionsWidth = additionsMaxWidth < additionsWidth ? additionsMaxWidth : additionsWidth;
						if ( additionsWidth < elipsisWidth )
							additionsWidth = 0;
						additionsMaxWidth = additionsWidth;

						totalIndent += additionsMaxWidth;
						prefixOffset += additionsMaxWidth;
						additionsOffset += leftOffset;
						break;

					case 'after' :
						additionsOffset += leftOffset + textWidth + prefixMaxWidth;
						break;
				}
			}
			prefixOffset += leftOffset;
			prefixMaxWidth = prefixMaxWidth < 0 ? 0 : prefixMaxWidth;
			totalIndent += prefixMaxWidth;

			let _css;

			if ( hasPrefixValue ) {
				_css = {};
				_css[ offsetPosition ] = prefixOffset + "px";
				_css['maxWidth'] = prefixMaxWidth + "px";
				$prefixElement.css( _css );
			}
			if ( hasAdditionsValue ) {
				_css = {};
				_css[ offsetPosition ] = additionsOffset + "px";
				_css['maxWidth'] = additionsMaxWidth + "px";
				$additionsElement.css( _css );
			}

			_css = {};
			_css['text-indent'] = totalIndent + "px";
			$input.css( _css );
		}
		const updatePlaceholder = function() {

			let $input = $doctitles,
				_placeholder = '';

			let _hasAdditionsValue = !! hoverAdditionsValue.length,
				_hasPrefixValue = !! hoverPrefixValue.length;

			let _hoverAdditionsValue = hoverAdditionsValue,
				_hoverPrefixValue = hoverPrefixValue;

			if ( tsf.states.isTermEdit ) {
				if ( tsf.params.termName ) {
					_hoverPrefixValue = isRTL ? ' :' + tsf.params.termName : tsf.params.termName + ': ';
					_hasPrefixValue = tsf.states.useTermPrefix;
				}
			}

			_placeholder = defaultTitle;

			if ( _hasPrefixValue ) {
				switch ( hoverPrefixPlacement ) {
					case 'before' :
						_placeholder = _hoverPrefixValue + _placeholder;
						break;

					case 'after' :
						_placeholder = _placeholder + _hoverPrefixValue;
						break;
				}
			}
			if ( _hasAdditionsValue ) {
				switch ( hoverAdditionsPlacement ) {
					case 'before' :
						_placeholder = _hoverAdditionsValue + _placeholder;
						break;

					case 'after' :
						_placeholder = _placeholder + _hoverAdditionsValue;
						break;
				}
			}

			//= Microsoft be like: "Let's spare 0.000000000073% of our money."
			if ( ie11 ) ie11killswitch = true;

			//= Converts special characters without running scripts.
			let phText = document.createElement( 'span' );
			phText.innerHTML = _placeholder;

			$input.prop( 'placeholder', phText.textContent );

			//= Promise.
			ie11 && setTimeout( function() {
				ie11killswitch = false;
			}, 0 );
		}
		const setReferenceTitle = function( event ) {
			let reference = document.getElementById( 'tsf-title-reference' ),
				text = '';

			if ( ! reference ) return;

			if ( event.target.value.length < 1 ) {
				text = event.target.placeholder;
			} else {
				//= We must determine the position as trailing whitespace is rendered 0.
				text = event.target.value;

				if ( hoverPrefixValue.length ) {
					switch ( hoverPrefixPlacement ) {
						case 'before' :
							text = hoverPrefixValue + text;
							break;

						case 'after' :
							text = text + hoverPrefixValue;
							break;
					}
				}
				if ( hoverAdditionsValue.length ) {
					switch ( hoverAdditionsPlacement ) {
						case 'before' :
							text = hoverAdditionsValue + text;
							break;

						case 'after' :
							text = text + hoverAdditionsValue;
							break;
					}
				}
			}

			reference.innerHTML = tsf.escapeString( text );
			// Fires change event. Defered.
			setTimeout( () => { jQuery( reference ).change() }, 0 );
		}
		const updateCounter = function( event ) {
			let counter = document.getElementById( event.target.id + '_chars' ),
				reference = document.getElementById( 'tsf-title-reference' );

			if ( ! counter || ! reference ) return;

			let titLen = tsf.getStringLength( tsf.unescapeString( reference.innerHTML ) ),
				target = event.target,
				counterClass = '',
				counterType = tsf.counterType,
				counterName = '',
				output = '';

			if ( titLen < 25 || titLen >= 75 ) {
				counterClass += 'tsf-count-bad';
				counterName = tsf.i18n.bad;
			} else if ( titLen < 42 || titLen > 55 ) {
				counterClass += 'tsf-count-okay';
				counterName = tsf.i18n.okay;
			} else {
				counterClass += 'tsf-count-good';
				counterName = tsf.i18n.good;
			}

			if ( ! counterType || 1 == counterType ) {
				output = titLen.toString();
			} else if ( 2 == counterType ) {
				output = counterName;
			} else if ( 3 == counterType ) {
				output = titLen.toString() + ' - ' + counterName;
			}

			counter.innerHTML = output;

			if ( tsf.additionsClass )
				counterClass += ' ' + tsf.additionsClass;

			if ( counter.className !== counterClass )
				counter.className = counterClass;
		}
		const updatePixels = function( event ) {
			let pixels = document.getElementById( event.target.id + '_pixels' ),
				reference = document.getElementById( 'tsf-title-reference' );

			if ( ! pixels || ! reference )
				return;

			let test = {
				'e': pixels,
				'text' : tsf.unescapeString( reference.innerHTML ),
				'guideline' : tsf.params.titlePixelGuideline,
			};

			tsf.updatePixelCounter( test );
		}

		/**
		 * Updates placements, placeholders and counters.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateTitlesTrigger = function( event ) {
			if ( ie11killswitch ) return false;
			updateHoverPlacement( event );
			updatePlaceholder();
			setReferenceTitle( event );
			updateCounter( event );
			updatePixels( event );
		}
		$doctitles.on( 'input.tsfUpdateTitles', updateTitlesTrigger );

		/**
		 * Updates character counters.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateCounterTrigger = function( event ) {
			setReferenceTitle( event );
			updateCounter( event );
			updatePixels( event );
		}
		$doctitles.on( 'tsf-update-title-counter', updateCounterTrigger );

		/**
		 * Triggers counter updates.
		 *
		 * @function
		 * @return {undefined}
		 */
		const triggerCounter = function() {
			$doctitles.trigger( 'tsf-update-title-counter' );
		}

		/**
		 * Triggers doctitles input.
		 *
		 * @function
		 * @return {undefined}
		 */
		const triggerInput = function() {
			$doctitles.trigger( 'input.tsfUpdateTitles' );
		}
		triggerInput();

		/**
		 * Triggers additions hover update on tagline change.
		 *
		 * @function
		 * @return {undefined}
		 */
		const updateTagline = function() {
			setHoverAdditionsValue();
			triggerInput();
		}
		jQuery( '#autodescription-site-settings\\[homepage_title_tagline\\]' ).on( 'input', updateTagline );
		jQuery( '#autodescription-site-settings\\[homepage_tagline\\]' ).on( 'change', updateTagline );

		let triggerBuffer = 0;
		/**
		 * Triggers doctitles input.
		 * @function
		 * @return {undefined}
		 */
		const enqueueTriggerInput = function() {
			clearTimeout( triggerBuffer );
			triggerBuffer = setTimeout( function() {
				triggerInput();
			}, 50 );
		}
		jQuery( window ).on( 'tsf-counter-updated', enqueueTriggerInput );

		/**
		 * Toggles tagline left/right example additions visibility for the homepage title.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const changeHomePageAdditionsVisibility = function( event ) {
			let prevUseTagline = useTagline;

			if ( jQuery( event.target ).is( ':checked' ) ) {
				jQuery( '.tsf-custom-blogname-js' ).css( 'display', 'inline' );
				useTagline = true;
			} else {
				jQuery( '.tsf-custom-blogname-js' ).css( 'display', 'none' );
				useTagline = false;
			}

			if ( prevUseTagline ^ useTagline ) {
				setHoverAdditionsValue();
				enqueueTriggerInput();
			}
		}
		jQuery( '#tsf-title-tagline-toggle :input' ).on( 'click', changeHomePageAdditionsVisibility );

		/**
		 * Updates private/protected title prefix upon Post visibility switch.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateVisibility = function( event ) {
			let value = jQuery( '#visibility' ).find( 'input:radio:checked' ).val();

			isPrivate = false;
			isPasswordProtected = false;

			switch ( value ) {
				case 'password' :
					let p = jQuery( '#visibility' ).find( '#post_password' ).val();
					isPasswordProtected = p ? !! p.length : false;
					break;

				case 'private' :
					isPrivate = true;
					break;

				default :
				case 'public' :
					break;
			}

			//* @TODO move all of the above to a global state handler?
			setHoverPrefixValue();
			enqueueTriggerInput();
		}
		jQuery( '#visibility .save-post-visibility' ).on( 'click', updateVisibility );

		/**
		 * Updates used separator and all examples thereof.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateSeparator = function( event ) {
			let val = jQuery( event.target ).val(),
				newSep = '';

			switch ( val ) {
				case 'pipe' :
					newSep = '|';
					break;

				case 'dash' :
					newSep = '-';
					break;

				default :
					newSep = jQuery( '<div/>' ).html( "&" + val + ";" ).text();
					break;
			}
			separator = newSep;
			jQuery( ".tsf-sep-js" ).text( ' ' + separator + ' ' );

			setHoverAdditionsValue();
			enqueueTriggerInput();
		}
		jQuery( '#tsf-title-separator :input' ).on( 'click', updateSeparator );

		/**
		 * Triggers title update, without affecting change listeners.
		 *
		 * @function
		 * @param {!jQuery.Event}
		 * @return {undefined}
		 */
		const triggerUnregisteredInput = function() {
			let settingsChangedCache = tsf.settingsChanged;
			triggerInput( true );
			tsf.settingsChanged = settingsChangedCache;
		}
		jQuery( '#homepage-tab-general' ).on( 'tsf-tab-toggled', triggerUnregisteredInput );
		jQuery( '#tsf-flex-inpost-tab-general' ).on( 'tsf-flex-tab-toggled', triggerUnregisteredInput );

		let unregisteredTriggerBuffer = 0;
		/**
		 * Enqueues doctitles input trigger.
		 * @function
		 * @return {undefined}
		 */
		const enqueueUnregisteredInputTrigger = function() {
			clearTimeout( unregisteredTriggerBuffer );
			unregisteredTriggerBuffer = setTimeout( triggerUnregisteredInput, 50 );
		}
		//= Defer to prevent early trigger.
		jQuery( window ).one( 'tsf-ready', function() {
			jQuery( window ).on( 'tsf-flex-resize', enqueueUnregisteredInputTrigger );
		} );

		let postboxIds = [ 'autodescription-homepage-settings', 'tsf-inpost-box' ];
		/**
		 * Enqueues doctitles input trigger synchronously.
		 * @function
		 * @param {!jQuery.Event} event
		 * @param {Element} elem
		 * @return {undefined}
		 */
		const triggerPostboxSynchronousUnregisteredInput = function( event, elem ) {
			if ( postboxIds.indexOf( elem.id ) >= 0 ) {
				let inside = elem.querySelector( '.inside' );
				if ( inside.offsetHeight > 0 && inside.offsetWidth > 0 ) {
					triggerUnregisteredInput();
				}
			}
		}
		jQuery( document ).on( 'postbox-toggled', triggerPostboxSynchronousUnregisteredInput );

		/**
		 * Triggers additions hover update on tagline placement change.
		 * @function
		 * @return {undefined}
		 */
		const setTaglinePlacement = function() {
			setHoverAdditionsPlacement();
			setHoverAdditionsValue();
			enqueueTriggerInput();
		}
		jQuery( '#tsf-home-title-location :input' ).on( 'click', setTaglinePlacement );

		/**
		 * Updates default title placeholder.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateDefaultTitle = function( event ) {
			let val = event.target.value;
			val = val.trim();

			if ( val.length ) {
				defaultTitle = tsf.escapeString( val );
			} else {
				defaultTitle = tsf.params.untitledTitle;
			}
			updatePlaceholder();
			triggerCounter();
		}
		//= The home page listens to a static preset value. Update all others.
		if ( ! tsf.states.isHome ) {
			jQuery( '#edittag #name, #titlewrap #title' ).on( 'input', updateDefaultTitle );
		}

		/**
		 * Triggers input event for titles in set intervals on window resize.
		 *
		 * This only happens if boundaries are surpassed to reduce CPU usage.
		 * This boundary is 782 pixels, because that forces input fields to change.
		 * in WordPress.
		 *
		 * @function
		 * @return {undefined}
		 */
		const prepareUnregisteredInputOnResize = function() {
			let resizeTimeout = 0,
				prevWidth = window.innerWidth;

			window.addEventListener( 'resize', function() {
				clearTimeout( resizeTimeout );
				resizeTimeout = setTimeout( function() {
					let width = window.innerWidth;
					if ( prevWidth < width ) {
						if ( prevWidth <= 782 && width >= 782 ) {
							triggerUnregisteredInput();
						}
					} else {
						if ( prevWidth >= 782 && width <= 782 ) {
							triggerUnregisteredInput();
						}
					}
					prevWidth = width;
				}, 50 );
			} );
		}
		prepareUnregisteredInputOnResize();
	},

	/**
	 * Initializes unbound title settings, which don't affect prefix/additions
	 * on-page.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {undefined}
	 */
	_initUnboundTitleSettings: function() {

		if ( ! tsf.hasInput )
			return;

		let $doctitles = jQuery( "#autodescription_title, #autodescription-meta\\[doctitle\\], #autodescription-site-settings\\[homepage_title\\]" );

		/**
		 * Makes user click act naturally by selecting the adjacent Title text
		 * input and move cursor all the way to the end.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const focusTitleInput = function( event ) {
			let input = jQuery( event.target ).siblings( $doctitles )[0];

			if ( 'function' === typeof input.setSelectionRange ) {
				input.focus();
				let length = input.value.length * 2;
				input.setSelectionRange( length, length );
			} else {
				//= Older browser compat.
				let val = input.value;
				input.value = '';
				input.focus();
				input.value = val;
			}
		}
		jQuery( '#tsf-title-placeholder, #tsf-title-placeholder-prefix' ).on( 'click', focusTitleInput );

		/**
		 * Triggers Change on Left/Right selection of global title options.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const toggleExampleDisplay = function( event ) {
			if ( jQuery( event.target ).is( ':checked' ) ) {
				jQuery( '.tsf-title-additions-js' ).css( 'display', 'none' );
			} else {
				jQuery( '.tsf-title-additions-js' ).css( 'display', 'inline' );
			}
		}
		jQuery( '#autodescription-site-settings\\[title_rem_additions\\]' ).on( 'click', toggleExampleDisplay );

		/**
		 * Toggles title additions location for the Title examples.
		 * There are two elements, rather than one. One is hidden by default.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const toggleExampleLocation = function( event ) {
			let $titleExampleLeft = jQuery( '.tsf-title-additions-example-left' ),
				$titleExampleRight = jQuery( '.tsf-title-additions-example-right' );

			if ( 'right' === jQuery( event.target ).val() ) {
				$titleExampleLeft.css( 'display', 'none' );
				$titleExampleRight.css( 'display', 'inline' );
			} else {
				$titleExampleLeft.css( 'display', 'inline' );
				$titleExampleRight.css( 'display', 'none' );
			}
		}
		jQuery( '#tsf-title-location input' ).on( 'click', toggleExampleLocation );

		/**
		 * Adjusts homepage left/right title example part.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const adjustHomepageExampleOutput = function( event ) {
			let val = event.target.value || '',
				$title = jQuery( '.tsf-custom-title-js' );

			if ( 0 === val.length ) {
				$title.text( tsf.params.defaultTitle );
			} else {
				$title.text( val );
			}
		};
		jQuery( '#autodescription-site-settings\\[homepage_title\\]' ).on( 'input', adjustHomepageExampleOutput );

		/**
		 * Adjusts homepage left/right title example part.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const adjustHomepageTaglineExampleOutput = function( event ) {
			let val = event.target.value || '',
				$tagline = jQuery( '.tsf-custom-tagline-js' );

			if ( 0 === val.length ) {
				$tagline.text( tsf.params.blogDescription );

				if ( 0 === tsf.params.blogDescription.length ) {
					jQuery( '#tsf-home-title-location .tsf-sep-js' ).hide();
				} else {
					jQuery( '#tsf-home-title-location .tsf-sep-js' ).show();
				}
			} else {
				$tagline.text( val );
				jQuery( '#tsf-home-title-location .tsf-sep-js' ).show();
			}
		};
		jQuery( '#autodescription-site-settings\\[homepage_title_tagline\\]' ).on( 'input.tsfInputTagline', adjustHomepageTaglineExampleOutput );
		jQuery( '#autodescription-site-settings\\[homepage_title_tagline\\]' ).trigger( 'input.tsfInputTagline' );

		/**
		 * Toggles title prefixes for the Prefix Title example.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const adjustPrefixExample = function( event ) {
			let $this = jQuery( event.target ),
				$prefix = jQuery( '.tsf-title-prefix-example' );

			if ( $this.is(':checked') ) {
				$prefix.css( 'display', 'none' );
			} else {
				$prefix.css( 'display', 'inline' );
			}
		}
		jQuery( '#title-prefixes-toggle :input' ).on( 'click', adjustPrefixExample );
	},

	/**
	 * Initializes all aspects for description editing.
	 * Assumes only one description editor is present in the DOM.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {undefined}
	 */
	_initDescInputs: function() {

		if ( ! tsf.hasInput )
			return;

		let $descriptions = jQuery( "#autodescription_description, #autodescription-meta\\[description\\], #autodescription-site-settings\\[homepage_description\\]" );

		if ( ! $descriptions.length )
			return;

		let separator = tsf.params.descriptionSeparator;

		/**
		 * Updates used separator and all examples thereof.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateSeparator = function( event ) {
			let val = jQuery( event.target ).val(),
				newSep = '';

			switch ( val ) {
				case 'pipe' :
					newSep = '|';
					break;

				case 'dash' :
					newSep = '-';
					break;

				default :
					newSep = jQuery( '<div/>' ).html( "&" + val + ";" ).text();
					break;
			}
			separator = newSep;
			jQuery( "#autodescription-descsep-js" ).text( ' ' + separator + ' ' );

			enqueueTriggerInput();
		}
		jQuery( '#tsf-description-separator input' ).on( 'click', updateSeparator );

		/**
		 * Updates used separator and all examples thereof.
		 *
		 * @since 3.0.4 : 1. Threshold "too long" has been increased from 155 to 300.
		 *                2. Threshold "far too long" has been increased to 330 from 175.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateCounter = function( event ) {

			let counter = document.getElementById( event.target.id + '_chars' );

			if ( ! counter )
				return;

			let descLen = 0,
				target = event.target,
				counterClass = '',
				counterType = tsf.counterType,
				counterName = '',
				output = '';

			if ( target.value.length < 1 ) {
				descLen = tsf.getStringLength( target.placeholder );
			} else {
				descLen = tsf.getStringLength( target.value );
			}

			if ( descLen < 100 || descLen >= 330 ) {
				counterClass += 'tsf-count-bad';
				counterName = tsf.i18n.bad;
			} else if ( descLen < 137 || descLen > 300 ) {
				counterClass += 'tsf-count-okay';
				counterName = tsf.i18n.okay;
			} else {
				counterClass += 'tsf-count-good';
				counterName = tsf.i18n.good;
			}

			if ( ! counterType || 1 == counterType ) {
				output = descLen.toString();
			} else if ( 2 == counterType ) {
				output = counterName;
			} else if ( 3 == counterType ) {
				output = descLen.toString() + ' - ' + counterName;
			}

			counter.innerHTML = output;

			if ( tsf.additionsClass )
				counterClass += ' ' + tsf.additionsClass;

			if ( counter.className !== counterClass )
				counter.className = counterClass;
		}
		const setReferenceDescription = function( event ) {
			let reference = document.getElementById( 'tsf-description-reference' ),
				text = '';

			if ( ! reference ) return;

			if ( event.target.value.length < 1 ) {
				text = event.target.placeholder;
			} else {
				text = event.target.value;
			}

			reference.innerHTML = tsf.escapeString( text );
			// Fires change event. Defered.
			setTimeout( () => { jQuery( reference ).change() }, 0 );
		}
		const updatePixels = function( event ) {
			let pixels = document.getElementById( event.target.id + '_pixels' ),
				reference = document.getElementById( 'tsf-description-reference' );

			if ( ! pixels || ! reference )
				return;

			let test = {
				'e': pixels,
				'text' : tsf.unescapeString( reference.innerHTML ),
				'guideline' : tsf.params.descPixelGuideline
			}

			tsf.updatePixelCounter( test );
		}

		/**
		 * Updates placements, placeholders and counters.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const updateDescriptionsTrigger = function( event ) {
			setReferenceDescription( event );
			updateCounter( event );
			updatePixels( event );
		}
		$descriptions.on( 'input.tsfUpdateDescriptions', updateDescriptionsTrigger );

		/**
		 * Triggers descriptions input.
		 *
		 * @function
		 * @return {undefined}
		 */
		const triggerInput = function() {
			$descriptions.trigger( 'input.tsfUpdateDescriptions' );
		}
		triggerInput();

		let triggerBuffer = 0;
		/**
		 * Triggers descriptions input.
		 * @function
		 * @return {undefined}
		 */
		const enqueueTriggerInput = function() {
			clearTimeout( triggerBuffer );
			triggerBuffer = setTimeout( function() {
				triggerInput();
			}, 50 );
		}
		jQuery( window ).on( 'tsf-counter-updated', enqueueTriggerInput );

		/**
		 * Triggers description input, without affecting change listeners.
		 *
		 * @function
		 * @param {!jQuery.Event}
		 * @return {undefined}
		 */
		const triggerUnregisteredInput = function() {
			let settingsChangedCache = tsf.settingsChanged;
			triggerInput();
			tsf.settingsChanged = settingsChangedCache;
		}

		let postboxIds = [ 'autodescription-homepage-settings', 'tsf-inpost-box' ];
		/**
		 * Enqueues description input trigger synchronously.
		 * @function
		 * @param {!jQuery.Event} event
		 * @param {Element} elem
		 * @return {undefined}
		 */
		const triggerPostboxSynchronousUnregisteredInput = function( event, elem ) {
			if ( postboxIds.indexOf( elem.id ) >= 0 ) {
				let inside = elem.querySelector( '.inside' );
				if ( inside.offsetHeight > 0 && inside.offsetWidth > 0 ) {
					enqueueTriggerInput();
				}
			}
		}
		jQuery( document ).on( 'postbox-toggled', triggerPostboxSynchronousUnregisteredInput );
	},

	/**
	 * Initializes social titles.
	 *
	 * @since 3.0.4
	 *
	 * @function
	 * @return {undefined}
	 */
	_initSocialTitleInputs: function() {

		if ( ! tsf.hasInput )
			return;

		let $ogTitle = jQuery( "#autodescription_og_title" ),
			$twTitle = jQuery( "#autodescription_twitter_title" ),
			$reference = jQuery( "#tsf-title-reference" );

		if ( ! $ogTitle.length || ! $twTitle.length || ! $reference.length )
			return;

		//= y u no fix dis, Microsoft. Crappy vars don't deserve CamelCase.
		let ie11killswitch = false,
			ie11 = !! navigator.userAgent.match(/Trident\/7\./);

		let ogTitleValue = tsf.escapeString( $ogTitle.val() ),
			twTitleValue = tsf.escapeString( $twTitle.val() ),
			referenceValue = $reference.text(); // already escaped.

		const getActiveValue = ( what ) => {
			let val = '';
			switch ( what ) {
				case 'twitter' :
					val = twTitleValue;
					// get next if not set.
				case 'og' :
					val = val.length ? val : ogTitleValue;
					// get next if not set.
				case 'reference' :
					val = val.length ? val : referenceValue;
			}
			return val;
		};
		const setPlaceholders = () => {
			if ( ie11 ) ie11killswitch = true;

			$ogTitle.attr( 'placeholder', getActiveValue( 'reference' ) );
			$twTitle.attr( 'placeholder', getActiveValue( 'og' ) );

			ie11 && setTimeout( function() {
				ie11killswitch = false;
			}, 0 );
		};
		const updateCounter = ( target, value, threshold ) => {
			if ( ! target || ! value || ! threshold ) {
				$ogTitle.each( ( i, el ) => updateCounter( el, getActiveValue( 'og' ), 88 ) );
				$twTitle.each( ( i, el ) => updateCounter( el, getActiveValue( 'twitter' ), 70 ) );
			} else {
				tsf.updateSocialCounter( target, value, threshold );
			}
		};
		$reference.on( 'change', () => {
			referenceValue = $reference.text();
			setPlaceholders();
			updateCounter();
		} );

		const updateOgTitle = ( event ) => {
			if ( ie11killswitch ) return;
			ogTitleValue = event.target.value.length ? tsf.escapeString( event.target.value ) : '';
			setPlaceholders();
			updateCounter();
		};
		const updateTwTitle = ( event ) => {
			if ( ie11killswitch ) return;
			twTitleValue = event.target.value.length ? tsf.escapeString( event.target.value ) : '';
			setPlaceholders();
			updateCounter();
		};
		$ogTitle.on( 'input.tsfUpdateOgTitle', updateOgTitle );
		$twTitle.on( 'input.tsfUpdateOgTitle', updateTwTitle );
	},

	/**
	 * Initializes social descriptions.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {undefined}
	 */
	_initSocialDescInputs: function() {

		if ( ! tsf.hasInput )
			return;

		let $ogDesc = jQuery( "#autodescription_og_description" ),
			$twDesc = jQuery( "#autodescription_twitter_description" ),
			$reference = jQuery( "#tsf-description-reference" );

		if ( ! $ogDesc.length || ! $twDesc.length || ! $reference.length )
			return;

		//= y u no fix dis, Microsoft. Crappy vars don't deserve CamelCase.
		let ie11killswitch = false,
			ie11 = !! navigator.userAgent.match(/Trident\/7\./);

		let ogDescValue = tsf.escapeString( $ogDesc.val() ),
			twDescValue = tsf.escapeString( $twDesc.val() ),
			referenceValue = $reference.text(); // already escaped.

		const getActiveValue = ( what ) => {
			let val = '';
			switch ( what ) {
				case 'twitter' :
					val = twDescValue;
					// get next if not set.
				case 'og' :
					val = val.length ? val : ogDescValue;
					// get next if not set.
				case 'reference' :
					val = val.length ? val : referenceValue;
			}
			return val;
		};
		const setPlaceholders = () => {
			if ( ie11 ) ie11killswitch = true;

			$ogDesc.attr( 'placeholder', getActiveValue( 'reference' ) );
			$twDesc.attr( 'placeholder', getActiveValue( 'og' ) );

			ie11 && setTimeout( function() {
				ie11killswitch = false;
			}, 0 );
		};
		const updateCounter = ( target, value, threshold ) => {
			if ( ! target || ! value || ! threshold ) {
				$ogDesc.each( ( i, el ) => updateCounter( el, getActiveValue( 'og' ), 300 ) );
				$twDesc.each( ( i, el ) => updateCounter( el, getActiveValue( 'twitter' ), 200 ) );
			} else {
				tsf.updateSocialCounter( target, value, threshold );
			}
		};
		$reference.on( 'change', () => {
			referenceValue = $reference.text();
			setPlaceholders();
			updateCounter();
		} );

		const updateOgDesc = ( event ) => {
			if ( ie11killswitch ) return;
			ogDescValue = event.target.value.length ? tsf.escapeString( event.target.value ) : '';
			setPlaceholders();
			updateCounter();
		};
		const updateTwDesc = ( event ) => {
			if ( ie11killswitch ) return;
			twDescValue = event.target.value.length ? tsf.escapeString( event.target.value ) : '';
			setPlaceholders();
			updateCounter();
		};
		$ogDesc.on( 'input.tsfUpdateOgDesc', updateOgDesc );
		$twDesc.on( 'input.tsfUpdateOgDesc', updateTwDesc );
	},

	/**
	 * Updates normalized counters for social input.
	 *
	 * @since 3.0.4
	 *
	 * @function
	 * @param {!Element} target The HMTL eleent.
	 * @param {string} value Must be escaped.
	 * @param {integer} threshold The "bad" threshold.
	 */
	updateSocialCounter: function( target, value, threshold ) {

		let counter = document.getElementById( target.id + '_chars' );

		if ( ! counter )
			return;

		let strLen = tsf.getStringLength( tsf.unescapeString( value ) ),
			counterClass = '',
			counterType = tsf.counterType,
			counterName = '',
			output = '';

		if ( strLen > threshold ) {
			counterClass += 'tsf-count-bad';
			counterName = tsf.i18n.bad;
		} else {
			counterClass += 'tsf-count-good';
			counterName = tsf.i18n.good;
		}

		if ( ! counterType || 1 == counterType ) {
			output = strLen.toString();
		} else if ( 2 == counterType ) {
			output = counterName;
		} else if ( 3 == counterType ) {
			output = strLen.toString() + ' - ' + counterName;
		}

		counter.innerHTML = output;

		if ( tsf.additionsClass )
			counterClass += ' ' + tsf.additionsClass;

		if ( counter.className !== counterClass )
			counter.className = counterClass;
	},

	/**
	 * Initializes counters.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {undefined}
	 */
	_initCounters: function() {

		if ( ! tsf.hasInput )
			return;

		/**
		 * Updates the counter type.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const counterUpdate = function( event ) {

			// Count up, reset to 0 if needed. We have 4 options: 0, 1, 2, 3
			tsf.counterType = tsf.counterType + 1;
			if ( tsf.counterType > 3 )
				tsf.counterType = 0;

			//* Update counters locally.
			updateCounterClasses();

			let target = '.tsf-counter-wrap .tsf-ajax',
				status = 0;

			//* Reset ajax loader
			tsf.resetAjaxLoader( target );

			//* Set ajax loader.
			tsf.setAjaxLoader( target );

			//* Setup external update.
			let settings = {
				method: 'POST',
				url: ajaxurl,
				datatype: 'json',
				data: {
					'action' : 'the_seo_framework_update_counter',
					'nonce' : tsf.nonces['edit_posts'],
					'val' : tsf.counterType,
				},
				async: true,
				success: function( response ) {

					/**
					 * @TODO convert to json header and/or test for availability of response.type before parsing?
					 * @see convertJSONResponse() @ https://github.com/sybrew/The-SEO-Framework-Extension-Manager/blob/master/lib/js/tsfem.js
					 * @see send_json() @ https://github.com/sybrew/The-SEO-Framework-Extension-Manager/blob/master/inc/classes/core.class.php
					 */
					response = jQuery.parseJSON( response );

					//* I could do value check, but that will simply lag behind. Unless an annoying execution delay is added.
					if ( 'success' === response.type )
						status = 1;

					switch ( status ) {
						case 0:
							tsf.unsetAjaxLoader( target, false );
							break;
						case 1:
							tsf.unsetAjaxLoader( target, true );
							break;
						default:
							tsf.resetAjaxLoader( target );
							break;
					}
				},
			}

			jQuery.ajax( settings );
		}
		jQuery( '.tsf-counter' ).on( 'click', counterUpdate );

		/**
		 * Sets up additionsClass variable.
		 * Also sets up browser caches correctly.
		 *
		 * @function
		 * @return {undefined}
		 */
		const updateCounterClasses = function() {

			let counterType = tsf.counterType;

			if ( 1 == counterType ) {
				tsf.additionsClass = 'tsf-counter-one';
				tsf.counterType = 1;
			} else if ( 2 == counterType ) {
				tsf.additionsClass = 'tsf-counter-two';
				tsf.counterType = 2;
			} else if ( 3 == counterType ) {
				tsf.additionsClass = 'tsf-counter-three';
				tsf.counterType = 3;
			} else {
				tsf.additionsClass = 'tsf-counter-zero';
				tsf.counterType = 0;
			}

			tsf._triggerCounterUpdate();
		}
		updateCounterClasses();

		/**
		 * Triggers displaying/hiding of character counters.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const toggleCharacterCounterDisplay = function( event ) {
			if ( jQuery( event.target ).is( ':checked' ) ) {
				jQuery( '.tsf-counter-wrap' ).show();
			} else {
				jQuery( '.tsf-counter-wrap' ).hide();
			}
		}
		jQuery( '#autodescription-site-settings\\[display_character_counter\\]' ).on( 'click', toggleCharacterCounterDisplay );

		/**
		 * Triggers displaying/hiding of character counters.
		 *
		 * @function
		 * @param {!jQuery.Event} event
		 * @return {undefined}
		 */
		const togglePixelCounterDisplay = function( event ) {
			if ( jQuery( event.target ).is( ':checked' ) ) {
				jQuery( '.tsf-pixel-counter-wrap' ).show();
				//= Pixels couldn't be counted when it was hidden.
				tsf._triggerCounterUpdate();
			} else {
				jQuery( '.tsf-pixel-counter-wrap' ).hide();
			}
		}
		jQuery( '#autodescription-site-settings\\[display_pixel_counter\\]' ).on( 'click', togglePixelCounterDisplay );
	},

	/**
	 * Initializes primary term selection.
	 *
	 * @since 3.0.0
	 * @since 3.0.4 1 : Added postbox toggle event listeners for help display correction.
	 *              2 : Added tab visibility checkers.
	 *
	 * @function
	 * @return {undefined}
	 */
	_initPrimaryTerm: function() {

		if ( ! tsf.hasInput || ! Object.keys( tsf.states.taxonomies ).length )
			return;

		let taxonomies = tsf.states.taxonomies,
			inputTemplate = wp.template( 'tsf-primary-term-selector' ),
			helpTemplate = wp.template( 'tsf-primary-term-selector-help' );

		let termSelector = document.createElement( 'span' );
		termSelector.classList.add( 'tsf-primary-term-selector' );
		termSelector.classList.add( 'tsf-set-primary-term' ); // newline for IE11 compat.

		(function(){
			let radio = document.createElement( 'input' );
			radio.setAttribute( 'type', 'radio' );
			termSelector.appendChild( radio );
		})();

		let input$ = {},
			checked$ = {},
			uniqueChecked$ = {},
			box$ = {},
			primaries = {};

		const addInput = function( taxonomy ) {
			let $wrap = jQuery( '#' + taxonomy + 'div' ),
				template = inputTemplate( { 'taxonomy' : taxonomies[ taxonomy ] } );
			$wrap.append( template );
		}
		const addHelp = function( taxonomy ) {
			let $wrap = jQuery( '#taxonomy-' + taxonomy ),
				template = helpTemplate( { 'taxonomy' : taxonomies[ taxonomy ] } );
			$wrap.append( template );
			fixHelpPos( taxonomy );
		}
		const fixHelpPos = function( taxonomy ) {
			let wrap = document.getElementById( 'taxonomy-' + taxonomy ),
				tabs = wrap.querySelectorAll( '.tabs-panel' );

			let $postbox = jQuery( wrap ).closest( '.postbox' );
			if ( $postbox.length && $postbox.hasClass( 'closed' ) ) return;

			let tab = Array.prototype.slice.call( tabs ).filter( function( el ) {
				return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
			} )[0];

			if ( ! tab ) return;

			let offset = tab.scrollHeight > tab.clientHeight
			           ? tab.offsetWidth - tab.clientWidth + 25 - 2 // 2px is padding or something?
			           : 25;

			if ( tsf.states.isRTL ) {
				wrap.querySelector( '.tsf-primary-term-selector-help-wrap' ).style.left = offset + 'px';
			} else {
				wrap.querySelector( '.tsf-primary-term-selector-help-wrap' ).style.right = offset + 'px';
			}
		}
		const fixHelpPosOnTabToggle = function( event ) {
			fixHelpPos( event.data.taxonomy );
		}
		const createSelector = function( taxonomy ) {
			let selector = termSelector.cloneNode( true );
			selector.setAttribute( 'title', taxonomies[ taxonomy ].i18n.makePrimary );
			selector.setAttribute( 'aria-label', taxonomies[ taxonomy ].i18n.makePrimary );
			return selector;
		}
		const setPostValue = function( taxonomy, value ) {
			let element = document.getElementById( 'autodescription[_primary_term_' + taxonomy + ']' );
			if ( element && element instanceof Element )
				element.value = value;
		}

		const getBox = function( taxonomy, reset ) {
			if ( ! reset && box$[ taxonomy ] )
				return box$[ taxonomy ];

			box$[ taxonomy ] = jQuery( '#' + taxonomy + 'checklist, #' + taxonomy + 'checklist-pop' );
			return box$[ taxonomy ];
		}
		const getInputWithVal = function( taxonomy, value ) {
			return input$[ taxonomy ].filter( '[value="' + value + '"]' );
		}

		const makePrimary = function( taxonomy, value ) {
			let $label = getInputWithVal( taxonomy, value ).closest( 'label' );
			if ( $label.length ) {
				$label.addClass( 'tsf-is-primary-term' );
				$label.find( '.tsf-set-primary-term' ).each( function( index, e ) {
					e.setAttribute( 'title', taxonomies[ taxonomy ].i18n.primary );
					e.setAttribute( 'aria-label', taxonomies[ taxonomy ].i18n.primary );
					e.querySelector( 'input' ).checked = true;
				} );
				setPostValue( taxonomy, value );
				primaries[ taxonomy ] = value;
			} else {
				makeFirstPrimary( taxonomy );
			}
		}
		const unsetPrimaries = function( taxonomy ) {
			let $label = getBox( taxonomy ).find( 'label' );
			$label.removeClass( 'tsf-is-primary-term' );
			$label.find( '.tsf-set-primary-term' ).each( function( index, e ) {
				e.setAttribute( 'title', taxonomies[ taxonomy ].i18n.makePrimary );
				e.setAttribute( 'aria-label', taxonomies[ taxonomy ].i18n.makePrimary );
				e.querySelector( 'input' ).checked = false;
			} );
			setPostValue( taxonomy, '' );
		}
		const makeFirstPrimary = function( taxonomy ) {
			let $checked = uniqueChecked$[ taxonomy ].first(),
				value;
			if ( $checked.length ) {
				value = $checked.val() || '';
				makePrimary( taxonomy, value );
				primaries[ taxonomy ] = value;
			}
		}

		const setPrimary = function( event ) {
			let taxonomy = event.data.taxonomy,
				value = jQuery( event.target ).closest( 'label' ).find( 'input[type=checkbox]' ).val();

			unsetPrimaries( taxonomy );
			makePrimary( taxonomy, value );

			//= Stop propagation
			return false;
		}
		const toggleShowSwitch = function( event ) {
			let taxonomy = event.data.taxonomy;

			if ( event.target.checked ) {
				addCheckedNode( taxonomy, event.target );
				appendButton( taxonomy, event.target );
			} else {
				removeCheckedNode( taxonomy, event.target );
				removeButton( taxonomy, event.target );
			}

			switch ( uniqueChecked$[ taxonomy ].length ) {
				case 0 :
					setPostValue( taxonomy, '' );
					break;

				case 1 :
					makeFirstPrimary( taxonomy );
					break;
			}
		}
		const appendButton = function( taxonomy, element ) {
			let $label;
			getInputWithVal( taxonomy, element.value ).each( function( index, e ) {
				$label = jQuery( e ).closest( 'label' );
				if ( ! $label.find( '.tsf-primary-term-selector' ).length ) {
					$label.append( createSelector( taxonomy ) );
				}
			} );
		}
		const removeButton = function( taxonomy, element ) {
			let $label, wasPrimary;
			getInputWithVal( taxonomy, element.value ).each( function( index, e ) {
				$label = jQuery( e ).closest( 'label' );
				wasPrimary = $label.hasClass( 'tsf-is-primary-term' );
				$label.removeClass( 'tsf-is-primary-term' );
				$label.find( '.tsf-primary-term-selector' ).remove();
				if ( wasPrimary ) makeFirstPrimary( taxonomy );
			} );
		}
		const addCheckedNode = function( taxonomy, element ) {
			checked$[ taxonomy ] = checked$[ taxonomy ].add( '[value="' + element.value + '"]' );
			uniqueChecked$[ taxonomy ] = uniqueChecked$[ taxonomy ].add( element );
		}
		const removeCheckedNode = function( taxonomy, element ) {
			checked$[ taxonomy ] = checked$[ taxonomy ].not( '[value="' + element.value + '"]' );
			uniqueChecked$[ taxonomy ] = uniqueChecked$[ taxonomy ].not( '[value="' + element.value + '"]' );
		}
		const togglePostbox = function( event, postbox ) {
			fixHelpPos( event.data.taxonomy );
		}

		const initVars = function( taxonomy ) {
			let $box = getBox( taxonomy, 1 );

			input$[ taxonomy ] = $box.find( 'input[type=checkbox]' );
			checked$[ taxonomy ] = $box.find( 'input[type=checkbox]:checked' );

			let found = {}, val;
			uniqueChecked$[ taxonomy ] = checked$[ taxonomy ];
			uniqueChecked$[ taxonomy ].each( function( index, element ) {
				val = jQuery( element ).val();
				if ( found[ val ] ) {
					uniqueChecked$[ taxonomy ].splice( index, 1 );
				} else {
					found[ val ] = true;
				}
			} );
		}
		const updateList = function( event, settings, wpList ) {
			if ( wpList.hasOwnProperty( 'settings' ) && wpList.settings.hasOwnProperty( 'what' ) ) {
				initVars( wpList.settings.what );
				initActions( wpList.settings.what );
				load( wpList.settings.what );
				fixHelpPos( wpList.settings.what );
			}
		}
		const initActions = function( taxonomy ) {
			let ns = 'tsfShowPrimary' + taxonomy,
				data = { 'taxonomy': taxonomy },
				$box = getBox( taxonomy ),
				$div = jQuery( '#' + taxonomy + 'div' ),
				$tabs = jQuery( '#' + taxonomy + '-tabs' ),
				$postbox = $box.closest( '.postbox' );

			$box.off( 'click.' + ns );
			$box.on( 'click.' + ns, 'input[type="checkbox"]', data, toggleShowSwitch );
			$box.on( 'click.' + ns, '.tsf-primary-term-selector', data, setPrimary );

			$div.off( 'wpListAddEnd.' + ns );
			$div.on( 'wpListAddEnd.' + ns, '#' + taxonomy + 'checklist', updateList );

			$tabs.off( 'click.' + ns );
			$tabs.on( 'click.' + ns, 'a', data, fixHelpPosOnTabToggle );

			$postbox.off( 'click.postboxes.' + ns );
			$postbox.on( 'click.postboxes.' + ns, data, togglePostbox );
		}
		const load = function( taxonomy ) {
			getBox( taxonomy ).find( 'input[type="checkbox"]:checked' )
				.each( function( index, element ) {
					appendButton( taxonomy, element );
				} );

			if ( taxonomies[ taxonomy ].primary ) {
				makePrimary( taxonomy, taxonomies[ taxonomy ].primary );
			} else {
				makeFirstPrimary( taxonomy );
			}
		}

		const init = function() {
			for ( let taxonomy in taxonomies ) {
				if ( getBox( taxonomy ).length ) {
					addInput( taxonomy );
					addHelp( taxonomy );
					initVars( taxonomy );
					initActions( taxonomy );
					load( taxonomy );
				}
			}
		}
		init();
	},

	/**
	 * Initializes status bar hover entries.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {undefined}
	 */
	_initToolTips: function() {

		let touchBuffer = 0,
			inTouchBuffer = false;

		const setTouchBuffer = function() {
			inTouchBuffer = true;
			clearTimeout( touchBuffer );
			touchBuffer = setTimeout( function() {
				inTouchBuffer = false;
			}, 250 );
		}

		const setEvents = function( target, unset ) {

			unset = unset || false;

			let touchEvents = 'pointerdown.tsfTT touchstart.tsfTT click.tsfTT',
				$target = jQuery( target );

			if ( unset ) {
				$target.off( 'mousemove mouseleave mouseout tsf-tooltip-update' );
				jQuery( document.body ).off( touchEvents );
			} else {
				$target.on( {
					'mousemove'  : mouseMove,
					'mouseleave' : mouseLeave,
					'mouseout'   : mouseLeave,
				} );
				jQuery( document.body ).off( touchEvents ).on( touchEvents, touchRemove );
			}

			$target.on( 'tsf-tooltip-update', updateDesc );
		}
		const unsetEvents = function( target ) {
			setEvents( target, true );
		}
		const updateDesc = function( event ) {
			if ( event.target.classList.contains( 'tsf-tooltip-item' ) ) {
				let tooltipText = event.target.querySelector( '.tsf-tooltip-text' );
				if ( tooltipText instanceof Element )
					tooltipText.innerHTML = event.target.dataset.desc;
			}
		}
		const mouseEnter = function( event ) {
			let $hoverItem = jQuery( event.target ),
				desc = event.target.dataset.desc;

			if ( desc && 0 === $hoverItem.find( 'div' ).length ) {
				//= Remove any titles attached.
				event.target.title = "";

				let $tooltip = jQuery(
						'<div class="tsf-tooltip"><span class="tsf-tooltip-text-wrap"><span class="tsf-tooltip-text">'
							+ desc +
						'</span></span><div class="tsf-tooltip-arrow"></div></div>'
					);
				$hoverItem.append( $tooltip );

				let $boundary = $hoverItem.closest( '.tsf-tooltip-boundary' );
				$boundary = $boundary.length && $boundary || jQuery( document.body );

				//= 9 = arrow (8) + shadow (1)
				let tooltipHeight = $hoverItem.outerHeight() + 9,
					tooltipTop = $tooltip.offset().top - tooltipHeight,
					boundaryTop = $boundary.offset().top - ( $boundary.prop( 'scrolltop' ) || 0 );

				if ( boundaryTop > tooltipTop ) {
					$tooltip.addClass( 'tsf-tooltip-down' );
					$tooltip.css( 'top', tooltipHeight + 'px' );
				} else {
					$tooltip.css( 'bottom', tooltipHeight + 'px' );
				}

				let $hoverItemWrap = $hoverItem.closest( '.tsf-tooltip-wrap' ),
					$textWrap = $tooltip.find( '.tsf-tooltip-text-wrap' ),
					$innerText = $textWrap.find( '.tsf-tooltip-text' ),
					hoverItemWrapWidth = $hoverItemWrap.width(),
					textWrapWidth = $textWrap.outerWidth( true ),
					textWidth = $innerText.outerWidth( true ),
					textLeft = $textWrap.offset().left,
					textRight = textLeft + textWidth,
					boundaryLeft = $boundary.offset().left - ( $boundary.prop( 'scrollLeft' ) || 0 ),
					boundaryRight = boundaryLeft + $boundary.outerWidth();

				//= RTL and LTR are normalized to abide to left.
				let direction = 'left';

				if ( textLeft < boundaryLeft ) {
					//= Overflown over left boundary (likely window)
					//= Add indent relative to boundary. 24px width of arrow / 2 = 12 middle
					let horIndent = boundaryLeft - textLeft + 12,
						basis = parseInt( $textWrap.css( 'flex-basis' ), 10 );

					/**
					 * If the overflow is greater than the tooltip flex basis,
					 * the tooltip was grown. Shrink it back to basis and use that.
					 */
					if ( horIndent < -basis )
						horIndent = -basis;

					$tooltip.css( direction, horIndent + 'px' );
					$tooltip.data( 'overflow', horIndent );
					$tooltip.data( 'overflowDir', direction );
				} else if ( textRight > boundaryRight ) {
					//= Overflown over right boundary (likely window)
					//= Add indent relative to boundary. Add 12px for visual appeal.
					let horIndent = boundaryRight - textRight - hoverItemWrapWidth - 12,
						basis = parseInt( $textWrap.css( 'flex-basis' ), 10 );

					/**
					 * If the overflow is greater than the tooltip flex basis,
					 * the tooltip was grown. Shrink it back to basis and use that.
					 */
					if ( horIndent < -basis )
						horIndent = -basis;

					$tooltip.css( direction, horIndent + 'px' );
					$tooltip.data( 'overflow', horIndent );
					$tooltip.data( 'overflowDir', direction );
				} else if ( hoverItemWrapWidth < 42 ) {
					//= Small tooltip container. Add indent to make it visually appealing.
					let indent = -15;
					$tooltip.css( direction, indent + 'px' );
					$tooltip.data( 'overflow', indent );
					$tooltip.data( 'overflowDir', direction );
				} else if ( hoverItemWrapWidth > textWrapWidth ) {
					//= Wrap is bigger than tooltip. Adjust accordingly.
					let pagex = event.originalEvent && event.originalEvent.pageX || event.pageX, // iOS touch support,
						hoverItemLeft = $hoverItemWrap.offset().left,
						center = pagex - hoverItemLeft,
						left = center - textWrapWidth / 2,
						right = left + textWrapWidth;

					if ( left < 0 ) {
						//= Don't overflow left.
						left = 0;
					} else if ( right > hoverItemWrapWidth ) {
						//= Don't overflow right.
						//* Use textWidth instead of textWrapWidth as it gets squashed in flex.
						left = hoverItemWrapWidth - textWidth;
					}

					$tooltip.css( direction, left + 'px' );
					$tooltip.data( 'adjust', left );
					$tooltip.data( 'adjustDir', direction );
				}
			}
		}
		const mouseMove = function( event ) {
			let $target = jQuery( event.target ),
				$tooltip = $target.find( '.tsf-tooltip' ),
				$arrow = $tooltip.find( '.tsf-tooltip-arrow' ),
				overflow = $tooltip.data( 'overflow' ),
				overflowDir = $tooltip.data( 'overflowDir' );

			overflow = parseInt( overflow, 10 );
			overflow = isNaN( overflow ) ? 0 : - Math.round( overflow );

			if ( overflow ) {
				//= Static arrow based on static overflow.
				$arrow.css( overflowDir, overflow + "px" );
			} else {
				let pagex = event.originalEvent && event.originalEvent.pageX || event.pageX, // iOS touch support
					arrowBoundary = 7,
					arrowWidth = 16,
					$hoverItemWrap = $target.closest( '.tsf-tooltip-wrap' ),
					mousex = pagex - $hoverItemWrap.offset().left - arrowWidth / 2,
					originalMousex = mousex,
					$textWrap = $tooltip.find( '.tsf-tooltip-text-wrap' ),
					textWrapWidth = $textWrap.outerWidth( true ),
					adjust = $tooltip.data( 'adjust' ),
					adjustDir = $tooltip.data( 'adjustDir' ),
					boundaryRight = textWrapWidth - arrowWidth - arrowBoundary;

				//= mousex is skewed, adjust.
				adjust = parseInt( adjust, 10 );
				adjust = isNaN( adjust ) ? 0 : Math.round( adjust );
				if ( adjust ) {
					adjust = 'left' === adjustDir ? -adjust : adjust;
					mousex = mousex + adjust;

					//= Use textWidth for right boundary if adjustment exceeds.
					if ( boundaryRight - adjust > $hoverItemWrap.outerWidth( true ) ) {
						let $innerText = $textWrap.find( '.tsf-tooltip-text' ),
							textWidth = $innerText.outerWidth( true );
						boundaryRight = textWidth - arrowWidth - arrowBoundary;
					}
				}

				if ( mousex <= arrowBoundary ) {
					//* Overflown left.
					$arrow.css( 'left', arrowBoundary + "px" );
				} else if ( mousex >= boundaryRight ) {
					//* Overflown right.
					$arrow.css( 'left', boundaryRight + "px" );
				} else {
					//= Somewhere in the middle.
					$arrow.css( 'left', mousex + "px" );
				}
			}
		}
		const mouseLeave = function( event ) {

			//* @see touchMove
			if ( inTouchBuffer )
				return;

			jQuery( event.target ).find( '.tsf-tooltip' ).remove();
			unsetEvents( event.target );
		}
		/**
		 * ^^^
		 * These two methods conflict eachother in EdgeHTML.
		 * Thusly, touch buffer.
		 * vvv
		 */
		const touchRemove = function( event ) {

			//* @see mouseLeave
			setTouchBuffer();

			let itemSelector = '.tsf-tooltip-item',
				balloonSelector = '.tsf-tooltip';

			let $target = jQuery( event.target ),
				$keepBalloon;

			if ( $target.hasClass( 'tsf-tooltip-item' ) ) {
				$keepBalloon = $target.find( balloonSelector );
			}
			if ( ! $keepBalloon ) {
				let $children = $target.children( itemSelector );
				if ( $children.length ) {
					$keepBalloon = $children.find( balloonSelector );
				}
			}

			if ( $keepBalloon && $keepBalloon.length ) {
				//= Remove all but this.
				jQuery( balloonSelector ).not( $keepBalloon ).remove();
			} else {
				//= Remove all.
				jQuery( balloonSelector ).remove();
			}
		}

		/**
		 * Loads tooltips within wrapper.
		 * @function
		 */
		const loadToolTip = function( event ) {

			if ( inTouchBuffer )
				return;

			let isTouch = false;

			switch ( event.type ) {
				case 'mouseenter' :
					//= Most likely, thus placed first.
					break;

				case 'pointerdown' :
				case 'touchstart' :
					isTouch = true;
					break;

				default :
					break;
			}

			if ( event.target.classList.contains( 'tsf-tooltip-item' ) ) {
				//= Removes previous items and sets buffer.
				isTouch && touchRemove( event );

				mouseEnter( event );
				//= Initiate placement directly for Windows Touch or when overflown.
				mouseMove( event );

				setEvents( event.target );
			} else {
				//= Delegate or bubble, and go back to this method with the correct item.
				let item = event.target.querySelector( '.tsf-tooltip-item:hover' ),
					_event = new jQuery.Event( event.type );

				_event.pageX = event.originalEvent && event.originalEvent.pageX || event.pageX;

				if ( item ) {
					if ( tsfL10n.states.debug ) console.log( 'Tooltip event warning: delegation' );
					jQuery( item ).trigger( _event );
				} else {
					if ( tsfL10n.states.debug ) console.log( 'Tooltip event warning: bubbling' );
					jQuery( event.target ).closest( '.tsf-tooltip-wrap' ).find( '.tsf-tooltip-item:hover' ).trigger( _event );
				}
			}

			//* Stop further propagation.
			event.stopPropagation();
		}

		/**
		 * Initializes tooltips.
		 * @function
		 */
		const initTooltips = function() {
			let $wrap = jQuery( '.tsf-tooltip-wrap' );

			$wrap.off( 'mouseenter pointerdown touchstart' );
			$wrap.on( 'mouseenter pointerdown touchstart', '.tsf-tooltip-item', loadToolTip );
		}
		initTooltips();
		jQuery( window ).on( 'tsf-reset-tooltips', initTooltips );

		(function() {
			let e = jQuery( '#wpcontent' );
			tsf.addTooltipBoundary( e );
		})();
	},

	/**
	 * Adds tooltip boundaries.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @param {!jQuery|Element} e
	 * @return {undefined}
	 */
	addTooltipBoundary: function( e ) {
		jQuery( e ).addClass( 'tsf-tooltip-boundary' );
	},

	/**
	 * Sets correct tab content and classes on toggle.
	 *
	 * @since 2.2.2
	 * @since 2.6.0 Improved.
	 * @since 2.9.0 Now always expects radio button input.
	 * @see tsf.setTabsOnload
	 *
	 * @function
	 * @param {!jQuery.Event} event
	 * @return {(undefined|null)}
	 */
	tabToggle: function( event ) {

		let $this = jQuery( event.target );

		if ( ! $this.is( ':checked' ) )
			return;

		let target = $this.prop( 'id' ),
			name = $this.prop( 'name' );

		if ( typeof name !== 'undefined' ) {
			let activeClass = 'tsf-active-tab-content',
				$newContent = jQuery( '#' + target + '-content' ),
				$previousContent = jQuery( '.' + activeClass );

			//* Only parse if old content isn't the new.
			if ( ! $newContent.is( $previousContent ) && typeof $newContent !== 'undefined' ) {
				let $allContent = jQuery( '.' + name + '-content' );

				$allContent.fadeOut( 150, function() {
					jQuery( this ).removeClass( activeClass );
				} );
				setTimeout( function() {
					$newContent.addClass( activeClass ).fadeIn( 250 );
				}, 150 );
				setTimeout( function() {
					jQuery( '#' + target ).trigger( 'tsf-tab-toggled' );
				}, 175 );
			}
		}
	},

	/**
	 * Refines Styling for the navigation tabs on the settings pages
	 *
	 * @since 2.9.0
	 * @todo merge with tabTobble or a collective method?
	 *
	 * @function
	 * @param {!jQuery.Event} event
	 * @return {(undefined|null)}
	 */
	flexTabToggle : function( event ) {

		let $this = jQuery( event.target );

		if ( ! $this.is( ':checked' ) )
			return;

		let target = $this.prop( 'id' ),
			name = $this.prop( 'name' );

		if ( typeof name !== 'undefined' ) {
			let activeClass = 'tsf-flex-tab-content-active',
				$newContent = jQuery( '#' + target + '-content' ),
				$previousContent = jQuery( '.' + activeClass );

			//* Only parse if old content isn't the new.
			if ( ! $newContent.is( $previousContent ) && typeof $newContent !== 'undefined' ) {
				let $allContent = jQuery( '.' + name + '-content' );

				$allContent.fadeOut( 150, function() {
					jQuery( this ).removeClass( activeClass );
				} );
				setTimeout( function() {
					$newContent.addClass( activeClass ).fadeIn( 250 );
				}, 150 );
				setTimeout( function() {
					jQuery( '#' + target ).trigger( 'tsf-flex-tab-toggled' );
				}, 175 );
			}
		}
	},

	/**
	 * Sets the navigation tabs content equal to the buttons.
	 *
	 * @since 2.9.0
	 * @since 3.0.4 Added inpost flex nav trigger.
	 * @see tsf.tabToggle
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	setTabsOnload: function() {

		if ( ! tsf.hasInput )
			return;

		if ( tsf.states['isPostEdit'] ) {
			//= Triggers inpost change event for tabs
			jQuery( '.tsf-flex-nav-tab-radio' ).trigger( 'change' );
		}

		if ( tsf.states['isSettingsPage'] ) {
			let $buttons = jQuery( '.tsf-nav-tab-wrapper .tsf-tab:nth-of-type(n+2) input:checked' );

			// Select all second or later tabs that have attribute checked.
			if ( $buttons.length ) {
				$buttons.each( function( i ) {
					let $this = jQuery( this ),
						target = $this.prop( 'id' ),
						name = $this.prop( 'name' );

					if ( typeof name !== 'undefined' ) {
						let activeClass = 'tsf-active-tab-content',
							$newContent = jQuery( '#' + target + '-content' );

						//* Only parse if old content isn't the new.
						if ( typeof $newContent !== 'undefined' ) {
							let $allContent = jQuery( '.' + name + '-content' );

							$allContent.removeClass( activeClass );
							$newContent.addClass( activeClass );
							setTimeout( function() {
								jQuery( '#' + target ).trigger( 'tsf-tab-toggled' );
							}, 20 );
						}
					}
				} );
			}
		} else {
			// WordPress resets radio buttons on inpost settings. Leave this open for "when".
		}
	},

	/**
	 * Toggle tagline within the Description Example.
	 *
	 * @since 2.3.4
	 *
	 * @function
	 * @param {!jQuery.Event} event
	 * @return {undefined}
	 */
	taglineToggleDesc: function( event ) {

		let $this = jQuery( event.target ),
			$tagDesc = jQuery( '#tsf-on-blogname-js' );

		if ( $this.is(':checked') ) {
			$tagDesc.css( 'display', 'inline' );
		} else {
			$tagDesc.css( 'display', 'none' );
		}
	},

	/**
	 * Toggle additions within Description example for the Example Description
	 *
	 * @since 2.6.0
	 *
	 * @function
	 * @param {!jQuery.Event} event
	 * @return {undefined}
	 */
	additionsToggleDesc: function( event ) {

		let $this = jQuery( event.target ),
			$tagDesc = jQuery( '#tsf-description-additions-js' );

		if ( $this.is(':checked') ) {
			$tagDesc.css( 'display', 'inline' );
		} else {
			$tagDesc.css( 'display', 'none' );
		}
	},

	/**
	 * Toggle tagline end examples within the Left/Right example for the
	 * HomePage Title or Description.
	 *
	 * @since 2.2.7
	 *
	 * @function
	 * @return {undefined}
	 */
	taglineToggleOnload: function() {

		if ( ! tsf.hasInput )
			return;

		let $tagTitle = jQuery( '#tsf-title-tagline-toggle :input' ),
			$title = jQuery( '.tsf-custom-blogname-js' ),
			$tagDescAdditions = jQuery( '#tsf-description-additions-toggle :input' ),
			$descAdditions = jQuery( '#tsf-description-additions-js' ),
			$tagDescBlogname = jQuery( '#tsf-description-onblogname-toggle :input' ),
			$descBlogname = jQuery( '#tsf-on-blogname-js' ),
			$tagTitleAdditions = jQuery( '#tsf-title-additions-toggle :input' ),
			$titleAdditions = jQuery( '.tsf-title-additions-js' );

		if ( $tagTitle.is( ':checked' ) ) {
			$title.css( 'display', 'inline' );
		} else {
			$title.css( 'display', 'none' );
		}

		if ( $tagDescAdditions.is( ':checked' ) ) {
			$descAdditions.css( 'display', 'inline' );
		} else {
			$descAdditions.css( 'display', 'none' );
		}

		if ( $tagDescBlogname.is( ':checked' ) ) {
			$descBlogname.css( 'display', 'inline' );
		} else {
			$descBlogname.css( 'display', 'none' );
		}

		// Reverse option.
		if ( $tagTitleAdditions.is( ':checked' ) ) {
			$titleAdditions.css( 'display', 'none' );
		} else {
			$titleAdditions.css( 'display', 'inline' );
		}
	},

	/**
	 * Have all form fields in The SEO Framework metaboxes set a dirty flag when changed.
	 *
	 * @since 2.0.0
	 * @since 2.9.3 No longer heavily invokes change listeners after change has been set.
	 *
	 * @function
	 * @return {undefined}
	 */
	attachUnsavedChangesListener: function() {

		if ( ! tsf.hasInput )
			return;

		//= Self calling and cancelling function.
		let setUnsetChange = (function( event ) {
			tsf.settingsChanged || tsf.registerChange();
			jQuery( input ).not( except ).off( event.type, setUnsetChange );
		});

		//= Mouse input
		let input = '.tsf-metaboxes :input, #tsf-inpost-box .inside :input',
			except = '.tsf-tab :input, .tsf-flex-nav-tab :input';
		jQuery( input ).not( except ).on( 'change', setUnsetChange );

		//= Text input
		input = '.tsf-metaboxes input[type=text], .tsf-metaboxes textarea, #tsf-inpost-box .inside input[type=text], #tsf-inpost-box .inside textarea';
		except = '.tsf-nav-tab-wrapper input, .tsf-flex-nav-tab-wrapper input';
		jQuery( input ).not( except ).on( 'input', setUnsetChange );

		//= Alert caller (doesn't work well when leave alerts have been disabled)
		window.onbeforeunload = function() {
			if ( tsf.settingsChanged ) {
				return tsf.i18n['saveAlert'];
			}
		};

		//= Remove alert on saving object or delete calls.
		jQuery( '.tsf-metaboxes input[type="submit"], #publishing-action input[type="submit"], #save-action input[type="submit"], a.submitdelete' ).click( function() {
			window.onbeforeunload = null;
		} );
	},

	/**
	 * Set a flag, to indicate form fields have changed.
	 *
	 * @since 2.2.4
	 *
	 * @function
	 * @return {undefined}
	 */
	registerChange: function() {
		tsf.settingsChanged = true;
	},

	/**
	 * Ask user to confirm that settings should now be reset.
	 *
	 * @since 2.2.4
	 *
	 * @function
	 * @return {(Boolean|null)} True if reset should occur, false if not.
	 */
	confirmedReset: function() {
		return confirm( tsf.i18n['confirmReset'] );
	},

	/**
	 * OnLoad changes can affect settings changes. This function reverts those.
	 *
	 * @since 2.5.0
	 *
	 * @function
	 * @return {undefined}
	 */
	onLoadUnregisterChange: function() {
		//* Prevent trigger of settings change
		tsf.settingsChanged = false;
	},

	/**
	 * Dismissible notices. Uses class .tsf-notice.
	 *
	 * @since 2.6.0
	 * @since 2.9.3 Now correctly removes the node from DOM.
	 *
	 * @function
	 * @param {!jQuery.Event} event
	 * @return {undefined}
	 */
	dismissNotice: function( event ) {
		jQuery( event.target ).parents( '.tsf-notice' ).slideUp( 200, function() {
			this.remove();
		} );
	},

	/**
	 * Visualizes AJAX loading time through target class change.
	 *
	 * @since 2.7.0
	 *
	 * @function
	 * @param {String} target
	 * @return {undefined}
	 */
	setAjaxLoader: function( target ) {
		jQuery( target ).toggleClass( 'tsf-loading' );
	},

	/**
	 * Adjusts class loaders on Ajax response.
	 *
	 * @since 2.7.0
	 *
	 * @function
	 * @param {String} target
	 * @param {Boolean} success
	 * @return {undefined}
	 */
	unsetAjaxLoader: function( target, success ) {

		let newclass = 'tsf-success',
			fadeTime = 2500;

		if ( ! success ) {
			newclass = 'tsf-error';
			fadeTime = 5000;
		}

		jQuery( target ).removeClass( 'tsf-loading' ).addClass( newclass ).fadeOut( fadeTime );
	},

	/**
	 * Cleans and resets Ajax wrapper class and contents to default.
	 * Also stops any animation and resets fadeout to beginning.
	 *
	 * @since 2.7.0
	 *
	 * @function
	 * @param {String} target
	 * @return {undefined}
	 */
	resetAjaxLoader: function( target ) {
		jQuery( target ).stop().empty().prop( 'class', 'tsf-ajax' ).css( 'opacity', '1' ).removeProp( 'style' );
	},

	/**
	 * Opens the image editor on request.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @param {!jQuery.Event} event jQuery event
	 * @return {(undefined|null)}
	 */
	openImageEditor: function( event ) {

		if ( jQuery( event.target ).prop( 'disabled' ) || 'undefined' === typeof wp.media ) {
			//* TODO error handling?
			event.preventDefault();
			event.stopPropagation();
			return;
		}

		let $target = jQuery( event.target ),
			inputID = $target.data( 'inputid' ),
			frame;

		if ( frame ) {
			frame.open();
			return;
		}

		event.preventDefault();
		event.stopPropagation();

		//* Init extend cropper.
		tsf.extendCropper();

		let _states = {
			suggestedWidth: $target.data( 'width' ) || 1200,
			suggestedHeight: $target.data( 'height' ) || 630,
			isFlex: typeof $target.data( 'flex' ) !== 'undefined' ? $target.data( 'flex' ) : 1,
		};

		tsf.cropper.control = {
			'params' : {
				'flex_width' : _states.isFlex ? 4096 : 0,
				'flex_height' : _states.isFlex ? 4096 : 0,
				'width' : _states.suggestedWidth,
				'height' : _states.suggestedHeight,
				'isFlex' : _states.isFlex,
			},
		};

		frame = wp.media( {
			button : {
				'text' : tsf.other[ inputID ]['frame_button'],
				'close' : false,
			},
			states: [
				new wp.media.controller.Library( {
					'title' : tsf.other[ inputID ]['frame_title'],
					'library' : wp.media.query({ 'type' : 'image' }),
					'multiple' : false,
					'date' : false,
					'priority' : 20,
					'suggestedWidth' : _states.suggestedWidth,
					'suggestedHeight' : _states.suggestedHeight
				} ),
				new tsf.cropper( {
					'imgSelectOptions' : tsf.calculateImageSelectOptions,
				} ),
			],
		} );

		const onSelect = (function() {
			frame.setState( 'cropper' );
		} );
		frame.off( 'select', onSelect );
		frame.on( 'select', onSelect );

		const onCropped = function( croppedImage ) {
			let url = croppedImage.url,
				attachmentId = croppedImage.id,
				w = croppedImage.width,
				h = croppedImage.height;

			// Send the attachment id to our hidden input. URL to explicit output.
			jQuery( '#' + inputID + '-url' ).val( url );
			jQuery( '#' + inputID + '-id' ).val( attachmentId );
		};
		frame.off( 'cropped', onCropped );
		frame.on( 'cropped', onCropped );

		const onSkippedCrop = function( selection ) {
			let url = selection.get( 'url' ),
				attachmentId = selection.get( 'id' ),
				w = selection.get( 'width' ),
				h = selection.get( 'height' );

			// Send the attachment id to our hidden input. URL to explicit output.
			jQuery( '#' + inputID + '-url' ).val( url );
			jQuery( '#' + inputID + '-id' ).val( attachmentId );
		};
		frame.off( 'skippedcrop', onSkippedCrop );
		frame.on( 'skippedcrop', onSkippedCrop );

		const onDone = function( imageSelection ) {
			jQuery( '#' + inputID + '-select' ).text( tsf.other[ inputID ]['change'] );
			jQuery( '#' + inputID + '-url' ).prop( 'readonly', true ).css( 'opacity', 0 ).animate(
				{ 'opacity' : 1 },
				{ 'queue' : true, 'duration' : 1000 },
				'swing'
			);

			tsf.appendRemoveButton( $target, inputID, true );
			tsf.registerChange();
		};
		frame.off( 'skippedcrop cropped', onDone );
		frame.on( 'skippedcrop cropped', onDone );

		frame.open();
	},

	/**
	 * Removes the image editor image on request.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @param {!jQuery.event.target} target jQuery event.target
	 * @param {string} inputID The input ID.
	 * @return {(undefined|null)}
	 */
	appendRemoveButton: function( target, inputID, animate ) {

		if ( target && inputID ) {
			if ( ! jQuery( '#' + inputID + '-remove' ).length ) {
				target.after(
					'<a href="javascript:void(0)" id="'
						+ inputID + '-remove" class="tsf-remove-social-image button button-small" data-inputid="'
						+ inputID +
					'" title="' + tsf.other[ inputID ]['remove_title'] + '">' + tsf.other[ inputID ]['remove'] + '</a>'
				);
				if ( animate ) {
					jQuery( '#' + inputID + '-remove' ).css( 'opacity', 0 ).animate(
						{ 'opacity' : 1 },
						{ 'queue' : true, 'duration' : 1000 },
						'swing'
					);
				}
			}
		}

		//* Reset cache.
		tsf.resetImageEditorActions();
	},

	/**
	 * Removes the image editor image on request.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @param {!jQuery.Event} event jQuery event
	 * @return {(undefined|null)}
	 */
	removeEditorImage: function( event ) {

		let inputID = jQuery( event.target ).data( 'inputid' );

		if ( jQuery( '#' + inputID + '-select' ).prop( 'disabled' ) )
			return;

		jQuery( '#' + inputID + '-select' ).addClass( 'disabled' ).prop( 'disabled', true );

		//* event.target.id === '#' + inputID + '-remove'.
		jQuery( '#' + inputID + '-remove' ).addClass( 'disabled' ).prop( 'disabled', true ).fadeOut( 500, function() {
			jQuery( this ).remove();
			jQuery( '#' + inputID + '-select' ).text( tsf.other[ inputID ]['select'] ).removeClass( 'disabled' ).removeProp( 'disabled' );
		} );

		let $inputUrl = jQuery( '#' + inputID + '-url' );

		$inputUrl.val( '' );
		if ( ! $inputUrl.data( 'readonly' ) ) {
			$inputUrl.removeProp( 'readonly' );
		}
		$inputUrl.css( 'opacity', 0 ).animate(
			{ 'opacity' : 1 },
			{ 'queue' : true, 'duration' : 500 },
			'swing'
		);

		jQuery( '#' + inputID + '-id' ).val( '' );

		tsf.registerChange();
	},

	/**
	 * Builds constructor for media cropper.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	extendCropper: function() {

		if ( 'undefined' !== typeof tsf.cropper.control )
			return;

		/**
		 * wp.media.controller.Cropper augmentation.
		 *
		 * A state for cropping an image.
		 *
		 * @class
		 * @augments wp.media.controller.Cropper
		 * @augments wp.media.controller.State
		 * @augments Backbone.Model
		 */
		let TSFCropper,
			Controller = wp.media.controller;

		/**
		 * wp.media.view.Cropper augmentation.
		 *
		 * @class
		 * @augments wp.media.View
		 * @augments wp.Backbone.View
		 * @augments Backbone.View
		 */
		let TSFView,
			View = wp.media.view;

		TSFView = View.Cropper.extend( {
			className: 'crop-content tsf-image',
			ready: function () {
				View.Cropper.prototype.ready.apply( this, arguments );
			},
			onImageLoad: function() {
				let imgOptions = this.controller.get( 'imgSelectOptions' ),
					imgSelect;

				if ( typeof imgOptions === 'function' ) {
					imgOptions = imgOptions( this.options.attachment, this.controller );
				}

				//= Seriously Core team, was this condition too hard to implement?
				if ( 'undefined' === typeof imgOptions.aspectRatio ) {
					imgOptions = _.extend( imgOptions, {
						parent: this.$el,
						onInit: function() {
							this.parent.children().on( 'mousedown touchstart', function( e ) {
								if ( e.shiftKey ) {
									imgSelect.setOptions( {
										aspectRatio: '1:1'
									} );
								} else {
									imgSelect.setOptions( {
										aspectRatio: false
									} );
								}
							} );
						}
					} );
				}
				this.trigger( 'image-loaded' );
				imgSelect = this.controller.imgSelect = this.$image.imgAreaSelect( imgOptions );
			},
		} );

		TSFCropper = Controller.Cropper.extend( {
			createCropContent: function() {
				this.cropperView = new TSFView( {
					controller: this,
					attachment: this.get( 'selection' ).first()
				} );
				this.cropperView.on( 'image-loaded', this.createCropToolbar, this );
				this.frame.content.set( this.cropperView );
			},
			doCrop: function( attachment ) {
				let cropDetails = attachment.get( 'cropDetails' ),
					control = tsf.cropper.control;

				// Use crop measurements when flexible in both directions.
				if ( control.params.flex_width && control.params.flex_height ) {
					// Square
					if ( cropDetails.width === cropDetails.height ) {
						if ( cropDetails.width > control.params.flex_width ) {
							cropDetails.dst_width = cropDetails.dst_height = control.params.flex_width;
						}
					// Landscape/Portrait
					} else {
						// Resize to flex width/height
						if ( cropDetails.width > control.params.flex_width || cropDetails.height > control.params.flex_height ) {
							// Landscape
							if ( cropDetails.width > cropDetails.height ) {
								let _ratio = cropDetails.width / control.params.flex_width;

								cropDetails.dst_width  = control.params.flex_width;
								cropDetails.dst_height = Math.round( cropDetails.height / _ratio );
							// Portrait
							} else {
								let _ratio = cropDetails.height / control.params.flex_height;

								cropDetails.dst_height = control.params.flex_height;
								cropDetails.dst_width  = Math.round( cropDetails.width / _ratio );
							}
						}
					}
				}

				// Nothing happened. Set destination to 0 and let PHP figure it out.
				if ( 'undefined' === typeof cropDetails.dst_width ) {
					cropDetails.dst_width  = 0;
					cropDetails.dst_height = 0;
				}

				return wp.ajax.post( 'tsf-crop-image', {
					'nonce' : tsf.nonces['upload_files'],
					'id' : attachment.get( 'id' ),
					'context' : 'tsf-image',
					'cropDetails' : cropDetails
				} );
			}
		} );

		TSFCropper.prototype.control = {};

		tsf.cropper = TSFCropper;

		return;
	},

	/**
	 * Returns a set of options, computed from the attached image data and
	 * control-specific data, to be fed to the imgAreaSelect plugin in
	 * wp.media.view.Cropper.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @param {wp.media.model.Attachment} attachment
	 * @param {wp.media.controller.Cropper} controller
	 * @return {Object} imgSelectOptions
	 */
	calculateImageSelectOptions: function( attachment, controller ) {
		let control = tsf.cropper.control;

		let flexWidth  = !! parseInt( control.params.flex_width, 10 ),
			flexHeight = !! parseInt( control.params.flex_height, 10 ),
			xInit = parseInt( control.params.width, 10 ),
			yInit = parseInt( control.params.height, 10 );

		let realWidth  = attachment.get( 'width' ),
			realHeight = attachment.get( 'height' ),
			ratio = xInit / yInit,
			xImg  = xInit,
			yImg  = yInit,
			x1,
			y1,
			imgSelectOptions;

		let canSkipCrop;
		if ( control.params.isFlex ) {
			canSkipCrop = ! tsf.mustBeCropped( control.params.flex_width, control.params.flex_height, realWidth, realHeight );
		} else {
			//= Not flex. If ratios match, then we can skip.
			canSkipCrop = ratio === realWidth / realHeight;
		}

		controller.set( 'control', control.params );
		controller.set( 'canSkipCrop', canSkipCrop );

		if ( realWidth / realHeight > ratio ) {
			yInit = realHeight;
			xInit = yInit * ratio;
		} else {
			xInit = realWidth;
			yInit = xInit / ratio;
		}

		x1 = ( realWidth - xInit ) / 2;
		y1 = ( realHeight - yInit ) / 2;

		imgSelectOptions = {
			'handles' : true,
			'keys' : true,
			'instance' : true,
			'persistent' : true,
			'imageWidth' : realWidth,
			'imageHeight' : realHeight,
			'minWidth' : xImg > xInit ? xInit : xImg,
			'minHeight' : yImg > yInit ? yInit : yImg,
			'x1' : x1,
			'y1' : y1,
			'x2' : xInit + x1,
			'y2' : yInit + y1
		};

		// @TODO Convert set img min-width/height to output ratio.
		// i.e. 200x2000 will become x = 1500/2000*200 = 150px, which is too small.
		//= Unlikely...

		if ( ! control.params.isFlex ) {
			imgSelectOptions.handles = 'corners';
			imgSelectOptions.aspectRatio = xInit + ':' + yInit;
		} else if ( ! flexHeight && ! flexWidth ) {
			imgSelectOptions.aspectRatio = xInit + ':' + yInit;
		} else {
			if ( flexHeight ) {
				imgSelectOptions.minHeight = 200;
				imgSelectOptions.maxWidth = realWidth;
			}
			if ( flexWidth ) {
				imgSelectOptions.minWidth = 200;
				imgSelectOptions.maxHeight = realHeight;
			}
		}

		return imgSelectOptions;
	},

	/**
	 * Return whether the image must be cropped, based on required dimensions.
	 * Disregards flexWidth/Height.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @param {Number} dstW
	 * @param {Number} dstH
	 * @param {Number} imgW
	 * @param {Number} imgH
	 * @return {Boolean}
	 */
	mustBeCropped: function( dstW, dstH, imgW, imgH ) {

		if ( imgW <= dstW && imgH <= dstH )
			return false;

		return true;
	},

	/**
	 * Resets jQuery image editor cache.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	resetImageEditorActions: function() {
		jQuery( '.tsf-remove-social-image' ).off( 'click', tsf.removeEditorImage );
		jQuery( '.tsf-remove-social-image' ).on( 'click', tsf.removeEditorImage );
	},

	/**
	 * Sets up jQuery image editor cache.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	setupImageEditorActions: function() {
		jQuery( '.tsf-set-social-image' ).off( 'click', tsf.openImageEditor );
		jQuery( '.tsf-remove-social-image' ).off( 'click', tsf.removeEditorImage );
		jQuery( '.tsf-set-social-image' ).on( 'click', tsf.openImageEditor );
		jQuery( '.tsf-remove-social-image' ).on( 'click', tsf.removeEditorImage );
	},

	/**
	 * Checks if input is filled in by image editor.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	checkImageEditorInput: function() {

		let $buttons = jQuery( '.tsf-set-social-image' );

		if ( $buttons.length ) {
			let inputID = '',
				$valID = '';

			jQuery.each( $buttons, function( index, value ) {
				inputID = jQuery( value ).data( 'inputid' );
				$valID = jQuery( '#' + inputID + '-id' );

				if ( $valID.length && $valID.val() > 0 ) {
					jQuery( '#' + inputID + '-url' ).prop( 'readonly', true );
					tsf.appendRemoveButton( jQuery( value ), inputID, false );
				}

				if ( jQuery( '#' + inputID + '-url' ).val() ) {
					jQuery( '#' + inputID + '-select' ).text( tsf.other[ inputID ]['change'] );
				}
			} );
		}
	},

	/**
	 * Enables wpColorPicker on input.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	setColorOnload: function() {

		let $selectors = jQuery( '.tsf-color-picker' );

		if ( $selectors.length ) {
			jQuery.each( $selectors, function( index, value ) {
				let $input = jQuery( value ),
					currentColor = '',
					defaultColor = $input.data( 'tsf-default-color' );

				$input.wpColorPicker( {
					'defaultColor' : defaultColor,
					'width' : 238,
					'change' : function( event, ui ) {
						currentColor = $input.wpColorPicker( 'color' );

						if ( '' === currentColor )
							currentColor = defaultColor;

						$input.val( currentColor );

						tsf.registerChange();
					},
					'clear' : function() {
						//* Privately marked WP class... open ticket?
						$input.parent().siblings( '.wp-color-result' ).css( 'backgroundColor', defaultColor );

						tsf.registerChange();
					},
					'palettes' : false,
				} );
			} );
		}
	},

	/**
	 * Registers on resize/orientationchange listeners and debounces to only run
	 * at intervals.
	 *
	 * For Flexbox implementation.
	 *
	 * @since 2.9.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	_doFlexResizeListener: function() {

		if ( ! jQuery( '.tsf-flex' ).length )
			return;

		//* Set event listeners.
		tsf._setResizeListeners();

		let resizeTimeout = 0,
			$lastWidth = {},
			timeOut = 0;

		// Warning: Only checks for the first item existence.
		let $tabWrapper = jQuery( '.tsf-flex-nav-tab-wrapper' ),
			$window = jQuery( window );

		$window.on( 'tsf-flex-resize', function() {

			clearTimeout( resizeTimeout );

			// Onload delays are 0, after than it's 10, 20 and 30 respectively.
			let _delay = 0;

			resizeTimeout = setTimeout( function() {
				if ( $tabWrapper.length ) {
					// Flex Tab Wrapper.
					let $innerWrap = jQuery( '.tsf-flex-nav-tab-inner' ),
						outerWrapWidth = $tabWrapper.width(),
						innerWrapWidth = $innerWrap.width(),
						$navName = jQuery( '.tsf-flex-nav-name' );

					if ( ! $lastWidth.tabWrapper ) {
						$lastWidth.tabWrapper = {};
						$lastWidth.tabWrapper.outer = 0;
						$lastWidth.tabWrapper.inner = 0;
						$lastWidth.tabWrapper.shown = 1;
					}

					// First run, revealed, or testing for new width. Either way, fadeIn.
					if ( ! $lastWidth.tabWrapper.shown && $lastWidth.tabWrapper.outer < outerWrapWidth ) {
						/**
						 * If ANYONE can find a way that doesn't make it flicker
						 * without using clones with stripped IDs/names, let me know.
						 * https://github.com/sybrew/the-seo-framework/issues/new
						 * https://github.com/sybrew/the-seo-framework/compare
						 */
						$navName.fadeIn( 250 );

						// Wait for 10 ms for slow browsers.
						setTimeout( function() {
							// Recalulate inner width (outer didn't change):
							innerWrapWidth = $innerWrap.width();
						}, _delay );
					}

					// Wait for an additional 10 ms for slow browsers.
					setTimeout( function() {
						if ( innerWrapWidth > outerWrapWidth ) {
							// Overflow (can be first run).
							$navName.hide();
							$lastWidth.tabWrapper.shown = 0;
						} else if ( $lastWidth.tabWrapper.outer < outerWrapWidth ) {
							// Grown or first run.
							$navName.fadeIn( 250 );
							$lastWidth.tabWrapper.shown = 1;
						}
					}, _delay * 2 );

					// Wait for an additional 10 ms for slow browsers.
					setTimeout( function() {
						$lastWidth.tabWrapper.outer = outerWrapWidth;
						$lastWidth.tabWrapper.inner = innerWrapWidth;
					}, _delay * 3 );
				}
			}, timeOut );

			// Update future timeouts.
			_delay = 10;
			timeOut = 75;
		} );

		//* Trigger after setup.
		$window.trigger( 'tsf-flex-resize' );
	},

	/**
	 * Sets flex resize listeners.
	 *
	 * @since 2.9.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	_setResizeListeners: function() {
		jQuery( window ).on( 'resize orientationchange', tsf._triggerResize );
		jQuery( '#collapse-menu' ).click( tsf._triggerResize );
		jQuery( '.columns-prefs :input[type=radio]' ).change( tsf._triggerResize );
		jQuery( '.meta-box-sortables' ).on( 'sortupdate', tsf._triggerResize );
	},

	/**
	 * Triggers tooltip reset.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	_triggerTooltipReset: function() {
		jQuery( window ).trigger( 'tsf-reset-tooltips' );
	},

	/**
	 * Triggers active tooltip update.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @param {Element} item
	 * @return {(undefined|null)}
	 */
	_triggerTooltipUpdate: function( item ) {
		jQuery( item ).trigger( 'tsf-tooltip-update' );
	},

	/**
	 * Triggers resize on event.
	 *
	 * @since 2.9.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	_triggerResize: function() {
		jQuery( window ).trigger( 'tsf-flex-resize' );
	},

	/**
	 * Triggers counter update event.
	 *
	 * @since 3.0.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	_triggerCounterUpdate: function() {
		jQuery( window ).trigger( 'tsf-counter-updated' );
	},

	/**
	 * Sets tsf.ready action.
	 *
	 * Example: jQuery( document.body ).on( 'tsf-ready', myFunc );
	 *
	 * @since 2.9.0
	 * @access private
	 *
	 * @function
	 */
	_triggerReady: function() {
		jQuery( document.body ).trigger( 'tsf-ready' );
	},

	/**
	 * Runs document-on-ready actions.
	 *
	 * @since 3.0.0
	 * @access private
	 *
	 * @function
	 */
	_doReady: function() {

		// Add counter listeners.
		tsf._initCounters();

		// Add title prop listeners. Must load before setTabsOnload to work.
		tsf._initTitleInputs();
		tsf._initUnboundTitleSettings();
		tsf._initSocialTitleInputs();

		// Add description prop listeners. Must load before setTabsOnload to work.
		tsf._initDescInputs();
		tsf._initSocialDescInputs();

		// Set primary term listeners.
		tsf._initPrimaryTerm();

		// Sets tabs to correct radio button on load.
		tsf.setTabsOnload();

		// Check if the Title Tagline or Description Additions should be removed when page is loaded.
		tsf.taglineToggleOnload();

		// Initialize the status bar hover balloon.
		tsf._initToolTips();

		// Initialize image uploader button cache.
		tsf.setupImageEditorActions();

		// Determine image editor button input states.
		tsf.checkImageEditorInput();

		// Correct Color Picker input
		tsf.setColorOnload();

		// #== End Before Change listener

		// Initialise form field changing flag.
		tsf.attachUnsavedChangesListener();

		// Deregister changes.
		tsf.onLoadUnregisterChange();

		// Do flex resize functionality.
		tsf._doFlexResizeListener();

		// Trigger tsf-ready event.
		tsf._triggerReady();

		// #== Start After Change listener
	},

	/**
	 * Sets up object parameters.
	 *
	 * @since 2.8.0
	 *
	 * @function
	 * @return {(undefined|null)}
	 */
	setupVars: function() {

		// The counter type. Mixed string and int (i10n is string, JS is int).
		tsf.counterType = parseInt( tsf.states['counterType'] );

		// Determines if the current page has input boxes for The SEO Framework.
		tsf.hasInput = tsf.states['hasInput'];
	},

	/**
	 * Initialises all aspects of the scripts.
	 *
	 * Generally ordered with stuff that inserts new elements into the DOM first,
	 * then stuff that triggers an event on existing DOM elements when ready,
	 * followed by stuff that triggers an event only on user interaction. This
	 * keeps any screen jumping from occuring later on.
	 *
	 * @since 2.2.4
	 * @since 2.7.0 jQuery object is now passed.
	 *
	 * @function
	 * @param {!jQuery} $ jQuery
	 * @return {undefined}
	 */
	ready: function( $ ) {

		// Set up object parameters.
		tsf.setupVars();

		// Move the page updates notices below the tsf-top-wrap.
		$( 'div.updated, div.error, div.notice-warning' ).insertAfter( 'div.tsf-top-wrap' );

		$( document.body ).ready( tsf._doReady );

		// Bind reset confirmation.
		$( '.tsf-js-confirm-reset' ).on( 'click', tsf.confirmedReset );

		// Toggle Tabs in the SEO settings page.
		$( '.tsf-tabs-radio' ).on( 'change', tsf.tabToggle );

		// Toggle Tabs for the inpost Flex settings.
		$( '.tsf-flex-nav-tab-radio' ).on( 'change', tsf.flexTabToggle );

		// Toggle Description additions removal.
		$( '#tsf-description-onblogname-toggle :input' ).on( 'click', tsf.taglineToggleDesc );
		$( '#tsf-description-additions-toggle :input' ).on( 'click', tsf.additionsToggleDesc );

		// Dismiss notices.
		$( '.tsf-dismiss' ).on( 'click', tsf.dismissNotice );
	}
};
jQuery( tsf.ready );

Zerion Mini Shell 1.0