Current File : /home/honehdyv/readbtooom.com/wp-content/plugins/autodescription/lib/js/tt.js |
/**
* This file holds Tooltips' code for adding on-hover balloons.
* Serve JavaScript as an addition, not as an ends or means.
*
* @author Sybre Waaijer <https://cyberwire.nl/>
* @link https://wordpress.org/plugins/autodescription/
*/
/**
* The SEO Framework plugin
* Copyright (C) 2019 - 2023 Sybre Waaijer, CyberWire B.V. (https://cyberwire.nl/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
/**
* Holds tsfTT (tsf tooltip) values in an object to avoid polluting global namespace.
*
* @since 3.1.0
*
* @constructor
*/
window.tsfTT = function() {
const _ttBase = 'tsf-tooltip';
const ttNames = {
base: _ttBase,
item: `${_ttBase}-item`,
wrap: `${_ttBase}-wrap`,
superWrap: `${_ttBase}-super-wrap`,
text: `${_ttBase}-text`,
textWrap: `${_ttBase}-text-wrap`,
boundary: `${_ttBase}-boundary`,
arrow: `${_ttBase}-arrow`,
}
// Yes, I'm too lazy to copy/paste whatever's above again to prepend a dot, so I spent half an hour figuring this.
const ttSelectors = Object.fromEntries( Object.entries( ttNames ).map( ( [ i, v ] ) => [ i, `.${v}` ] ) );
const _activeToolTipHandles = {
updateDesc: event => {
if ( ! event.target.classList.contains( ttNames.item ) ) return;
let tooltipText = event.target.querySelector( ttSelectors.text );
if ( tooltipText instanceof Element ) {
tooltipText.innerHTML = event.target.dataset.desc;
event.target.dispatchEvent( new Event( 'mousemove' ) ); // performance: <.3ms
}
},
pointerEnter: async event => {
let desc = event.target.dataset.desc || event.target.title || '';
// Don't create tooltip if bubbled.
if ( desc && ! event.target.getElementsByClassName( ttNames.base ).length ) {
// Exchanges data-desc with found desc to sustain easy access.
event.target.dataset.desc = desc;
// Clear title to prevent default browser tooltip.
event.target.removeAttribute( 'title' );
return await doTooltip( event, event.target, desc );
}
return false;
},
pointerMove: event => {
_pointer.currPos.x = event.pageX || NaN;
_pointer.lastMoveEvent = event;
},
pointerLeave: event => {
removeTooltip( event.target );
_events( event.target ).unset();
// Simply continue the animation if we continue onto another tooltip item.
// If relatedTarget doesn't exist, also cancel.
if ( ! event.relatedTarget?.classList?.contains( ttNames.item ) )
_cancelArrowAnimation();
},
}
const _events = target => {
const commonEvents = {
mousemove: _activeToolTipHandles.pointerMove,
mouseleave: _activeToolTipHandles.pointerLeave,
mouseout: _activeToolTipHandles.pointerLeave,
blur: _activeToolTipHandles.pointerLeave,
};
return {
set: () => {
for ( const [ event, callBack ] of Object.entries( commonEvents ) ) {
target.addEventListener( event, callBack );
}
target.addEventListener( 'tsf-tooltip-update', _activeToolTipHandles.updateDesc );
},
unset: () => {
for ( const [ event, callBack ] of Object.entries( commonEvents ) ) {
target.removeEventListener( event, callBack );
}
},
};
}
const _activeTooltipElements = {
tooltip: void 0,
arrow: void 0,
wrap: void 0,
reset: () => {
_activeTooltipElements.tooltip = _activeTooltipElements.arrow = _activeTooltipElements.wrap = void 0;
}
};
const _pointer = {
lastPos: { x: void 0 },
currPos: { x: void 0 },
lastMoveEvent: void 0,
reset: () => {
_pointer.lastMoveEvent = void 0;
// Before and after should have objects assigned separately.
// For otherwise they get the same pointer. Yes, a memory pointer: reference.
_pointer.currPos = { x: void 0 };
_pointer.lastPos = { x: void 0 };
}
}
const {
_requestArrowAnimation,
_cancelArrowAnimation,
_requestArrowAnimationOnce,
} = ( () => {
let _pointerAnimationId = void 0;
const _requestArrowAnimation = () => {
_pointerAnimationId = requestAnimationFrame( animate );
}
const _cancelArrowAnimation = () => {
cancelAnimationFrame( _pointerAnimationId );
_pointer.lastMoveEvent = void 0;
_activeTooltipElements.reset();
_pointer.reset();
}
const _requestArrowAnimationOnce = () => {
animate();
_cancelArrowAnimation();
}
const animate = () => {
let isMouseEvent = ! [ _pointer.currPos.x ].includes( NaN );
if ( isMouseEvent ) {
if ( _pointer.currPos.x === _pointer.lastPos.x ) {
_requestArrowAnimation();
return;
}
}
_pointer.lastPos.x = _pointer.currPos.x;
const event = _pointer.lastMoveEvent,
element = event.target;
let tooltip = _activeTooltipElements.tooltip || ( element.querySelector( ttSelectors.base ) );
// Browser lagged, no tooltip exists (yet). Bail.
if ( ! tooltip ) {
_requestArrowAnimation();
return;
}
_activeTooltipElements.tooltip ||= tooltip;
_activeTooltipElements.arrow ||= tooltip.querySelector( ttSelectors.arrow );
_activeTooltipElements.wrap ||= element.closest( ttSelectors.wrap ) || element.parentNode;
let pagex = _pointer.currPos.x;
if ( 'focus' === event.type ) {
// Grab the middle of the item on focus.
pagex = element.getBoundingClientRect().left + ( element.offsetWidth / 2 );
} else if ( isNaN( pagex ) ) {
// Get the last known tooltip position on manual tooltip alteration.
pagex = _activeTooltipElements.tooltip.dataset.lastPagex || element.getBoundingClientRect().left;
}
// Keep separate record of pagex, so updateDesc() can utilize this via isNaN hereabove.
_activeTooltipElements.tooltip.dataset.lastPagex = pagex;
const textWrap = _activeTooltipElements.tooltip.querySelector( ttSelectors.textWrap ),
arrowBoundary = 7,
arrowWidth = 16;
let mousex = pagex - _activeTooltipElements.wrap.getBoundingClientRect().left - ( arrowWidth / 2 ),
adjust = _activeTooltipElements.tooltip.dataset.adjust,
boundaryRight = textWrap.offsetWidth - arrowWidth - arrowBoundary;
// mousex is skewed, adjust.
adjust = parseInt( adjust, 10 );
adjust = isNaN( adjust ) ? 0 : Math.round( adjust );
if ( adjust ) {
mousex = mousex - adjust;
// Use textWidth for right boundary if adjustment exceeds.
if ( boundaryRight + adjust > _activeTooltipElements.wrap.offsetWidth ) {
let innerText = textWrap.querySelector( ttSelectors.text ),
textWidth = innerText.offsetWidth;
boundaryRight = textWidth - arrowWidth - arrowBoundary;
}
}
if ( mousex <= arrowBoundary ) {
// Overflown left.
_activeTooltipElements.arrow.style.left = `${arrowBoundary}px`;
} else if ( mousex >= boundaryRight ) {
// Overflown right.
_activeTooltipElements.arrow.style.left = `${boundaryRight}px`;
} else {
// Somewhere in the middle.
_activeTooltipElements.arrow.style.left = `${mousex}px`;
}
if ( isMouseEvent ) {
_requestArrowAnimation();
} else {
_pointerAnimationId && _cancelArrowAnimation();
}
}
return {
_requestArrowAnimation,
_cancelArrowAnimation,
_requestArrowAnimationOnce,
};
} )();
const _clickLocker = element => {
return {
lock: () => {
element.dataset.preventedClick = 1;
// If the element is a label with a "for"-attribute, then we must forward this
if ( element instanceof HTMLLabelElement && element.htmlFor ) {
let input = document.getElementById( element.htmlFor );
if ( input ) input.dataset.preventedClick = 1;
}
if ( element instanceof HTMLInputElement && element.id ) {
document.querySelectorAll( `label[for="${element.id}"]` ).forEach(
label => { label.dataset.preventedClick = 1; }
);
}
},
release: () => {
if ( ! ( element instanceof Element ) ) return;
delete element.dataset.preventedClick;
if ( element instanceof HTMLLabelElement && element.htmlFor ) {
let input = document.getElementById( element.htmlFor );
if ( input ) delete input.dataset.preventedClick;
}
if ( element instanceof HTMLInputElement && element.id ) {
document.querySelectorAll( `label[for="${element.id}"]` ).forEach(
la => { delete la.dataset.preventedClick; }
);
}
},
isLocked: () => element instanceof Element && !!+element.dataset.preventedClick,
}
}
/**
* Initializes tooltips.
*
* @since 3.1.0
* @since 4.0.0 1. Now adds default boundary to `wpwrap` instead of `wpcontent`.
* 2. Added focus/blur support.
* @since
* @access private
*
* @function
* @param {event?} event
* @param {Element} element
* @param {string} desc
*/
const _initToolTips = () => {
// TODO move this test to the main tsf object? This whole file doesn't rely on `window.tsf` though.
let passiveSupported = false,
captureSupported = false;
/**
* Sets passive & capture support flag.
* @link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*/
try {
( () => {
const options = {
get passive() {
passiveSupported = true;
return false;
},
get capture() {
captureSupported = true;
return false;
},
};
// These EventTarget methods will try to get 'passive' and/or 'capture' when it's supported.
window.addEventListener( 'tsf-tt-test-passive', null, options );
window.removeEventListener( 'tsf-tt-test-passive', null, options );
} )();
} catch ( e ) {
passiveSupported = false;
captureSupported = false;
}
/**
* Loads tooltips within wrapper.
* @function
* @param {Event} event
*/
const loadToolTip = async event => {
if ( event.target.dataset.hasTooltip ) return;
let isTouch = false;
switch ( event.type ) {
case 'mouseenter':
// Most likely, thus placed first.
break;
case 'pointerdown':
case 'touchstart':
isTouch = true;
break;
case 'focus':
default:
break;
}
if ( ! isTouch )
_clickLocker( event.target ).lock();
_cancelArrowAnimation();
if ( ! ( await _activeToolTipHandles.pointerEnter( event ) ) ) return;
// Initiate arrow placement directly.
_activeToolTipHandles.pointerMove( event );
if ( isTouch ) {
_requestArrowAnimationOnce();
} else {
_requestArrowAnimation();
}
// Set other events, like removal when tapping elsewhere, or hitting "tab."
_events( event.target ).set();
}
/**
* Prevents default click action on a tooltip item.
*
* Doesn't test whether a tooltip is present, since that happens asynchronously--often (yet _not always_) after the click finishes.
* If we set a datapoint where we tell the tooltip is still building, we might be able to read that out (e.g. instigatingTooltip).
*
* @function
* @param {Event} event
*/
const preventTooltipHandleClick = event => {
if ( _clickLocker( event.target ).isLocked() ) return;
event.preventDefault();
// iOS 12 bug causes two clicks at once. Let's set this asynchronously.
setTimeout( () => _clickLocker( event.target ).lock() );
}
let instigatingTooltip = false;
/**
* Handles earliest stages of the tooltip.
*
* Note to self: Don't debounce using timeouts!
* Even at 144hz (7ms) it makes the tt flicker obviously when traveling over the SEO Bar.
*
* @function
* @param {Event} event
*/
const handleToolTip = event => {
if ( instigatingTooltip ) return;
instigatingTooltip = true;
if ( event.target.classList.contains( ttNames.item ) )
loadToolTip( event );
event.stopPropagation();
instigatingTooltip = false;
}
const options = passiveSupported && captureSupported ? { capture: true, passive: true } : true;
/**
* Initializes tooltips.
* @function
*/
const init = () => {
let wraps = document.querySelectorAll( ttSelectors.wrap ),
actions = 'mouseenter pointerdown touchstart focus'.split( ' ' );
for ( let i = 0; i < wraps.length; i++ ) {
actions.forEach( e => {
wraps[ i ].addEventListener( e, handleToolTip, options );
} );
// NOTE: If the tooltip-wrap is a label with a "for"-attribute, the input is forwarded to the <input> field.
// We mitigated this issue at loadToolTip().
wraps[ i ].addEventListener(
'click',
preventTooltipHandleClick,
captureSupported ? { capture: false } : false
);
}
}
window.addEventListener( 'tsf-tooltip-reset', init );
triggerReset();
addBoundary( '#wpwrap' ); //! All pages, but Gutenberg destroys the boundaries.. @see tsfGBC
}
/**
* Renders tooltip.
*
* @since 4.2.0
* @access private
*
* @function
* @param {event?} event Optional. The current mouse/touch event to center
* tooltip position for to make it seem more natural.
* @param {Element} element The element to add the tooltip to.
* @param {string} desc The tooltip, may contain renderable HTML.
* @return {Boolean} True on success, false otherwise.
*/
const _renderTooltip = ( event, element, desc ) => {
element.dataset.hasTooltip = 1;
const tooltip = document.createElement( 'div' );
tooltip.classList.add( ttNames.base );
tooltip.insertAdjacentHTML(
'afterbegin',
`<span class=${ttNames.textWrap}><span class=${ttNames.text}>${desc}</span></span><div class=${ttNames.arrow} style=will-change:left></div>`
);
element.prepend( tooltip );
const boundary = element.closest( ttSelectors.boundary )
|| element.closest( '.edit-post-sidebar' ) // Gutenberg Sidebar
// || element.closest( '.postbox-container' ) // Gutenberg Bottom (doesn't seem necessary)
|| document.getElementById( 'wpcontent' )
|| document.body;
const boundaryRect = boundary.getBoundingClientRect(),
boundaryTop = boundaryRect.top - ( boundary.scrollTop || 0 ),
boundaryWidth = boundaryRect.width,
maxWidth = 250; // Gutenberg is 262. The tooltip has 24px padding (12*2)...
const hoverItemSuperWrap = element.closest( ttSelectors.superWrap ),
hoverItemWrap = element.closest( ttSelectors.wrap ) || element.parentElement,
textWrap = tooltip.querySelector( ttSelectors.textWrap );
const superWrapRect = hoverItemSuperWrap?.getBoundingClientRect(),
hoverItemWrapRect = hoverItemWrap.getBoundingClientRect();
let textWrapRect;
const resetTextRects = () => {
textWrapRect = textWrap.getBoundingClientRect();
};
resetTextRects();
let appeal = 12, // equals parseInt( getComputedStyle( textWrap ).paddingRight ),
horIndent = 0;
// Calculate the appeal with the spacing.
if ( textWrapRect.width > ( boundaryWidth - ( appeal / 2 ) ) ) {
// Overflown the boundary size. Squeeze the box. (thank you, Gutenberg.)
// Use the bounding box minus appeal. Don't double the appeal since that'll mess up the arrow.
// Maximum 250px.
textWrap.style.flexBasis = `${Math.min( maxWidth, boundaryWidth - appeal )}px`;
resetTextRects();
// Halve appeal from here. So each side gets a bit.
appeal /= 2;
} else if ( textWrapRect.width > maxWidth ) {
textWrap.style.flexBasis = `${maxWidth}px`; // Is this redundant?
// Limit the text wrap if it exceeds 250px on auto-grow. Flex ignores box-sizing?
textWrap.style.maxWidth = `${maxWidth}px`;
resetTextRects();
} else {
// Text wrap is small. Halve the appeal.
appeal /= 2;
}
const boundaryLeft = boundaryRect.left - ( boundary.scrollLeft || 0 ),
boundaryRight = boundaryLeft + boundaryWidth;
const textWrapWidth = textWrapRect.width,
textBorderLeft = textWrapRect.left,
textBorderRight = textBorderLeft + textWrapWidth,
wrapperWidth = superWrapRect?.width || hoverItemWrapRect.width;
if ( textBorderLeft < boundaryLeft ) {
// Overflown over left boundary (likely window)
// Add indent relative to boundary.
horIndent = boundaryLeft - textBorderLeft + appeal;
} else if ( textBorderRight > boundaryRight ) {
// Overflown over right boundary (likely window)
// Add indent relative to boundary minus text wrap width.
horIndent = boundaryRight - textBorderLeft - textWrapWidth - appeal;
} else if ( wrapperWidth < 42 ) {
// Small tooltip container. Add indent relative to the item to make it visually appealing.
horIndent = ( -wrapperWidth / 2 ) - appeal;
} else if ( wrapperWidth > textWrapWidth ) {
// Wrap is larger than tooltip. Find middle of pointer (if any) and adjust accordingly.
let pagex = event?.pageX || NaN; // This will be NaN if 0 -- which could never happen anyway.
if ( 'focus' === event?.type ) {
// No pointer-event found. Set indent to the middle instead.
horIndent = ( wrapperWidth / 2 ) - ( textWrapWidth / 2 );
} else if ( isNaN( pagex ) ) {
horIndent = -appeal;
} else {
// Set to middle of pointer.
horIndent = pagex - hoverItemWrapRect.left - ( textWrapWidth / 2 ) + appeal;
}
// We work from left=0, so to get to the right, we need the width.
let appealLeft = -appeal,
appealRight = wrapperWidth - textWrapWidth + appeal;
if ( horIndent < appealLeft ) {
// Overflown left more than appeal, let's move it more over the hoverwrap.
horIndent = appealLeft;
}
if ( horIndent > appealRight ) {
// Overflown right more than appeal, let's move it more over the hoverwrap.
horIndent = appealRight;
}
} else {
// Shift it a bit.
horIndent = window.isRTL ? appeal : -appeal;
}
if ( ( horIndent + textBorderLeft ) < ( boundaryLeft + appeal ) ) {
// Overflows left boundary's appeal. Use half appeal to shift it back a bit.
horIndent += appeal / 2;
}
if ( ( horIndent + textBorderRight ) > ( boundaryRight + appeal ) ) {
// FIXME?: Remove? We were unable to reach this code in our testing; even RTL.
// Overflows right boundary's appeal. Use half appeal to shift it back a bit.
horIndent -= appeal / 2;
}
if ( ( horIndent + textBorderLeft ) < boundaryLeft ) {
// It failed again after alignment. Reset to 0.
horIndent = 0;
}
if ( ! event ) {
let basis = parseInt( textWrap.style.flexBasis, 10 );
/**
* If the indent overflow is greater than the tooltip flex basis,
* the tooltip was repainted and shrunk. It may shrink beyond the horIndent,
* causing a misplaced box; so, we replace that with the basis.
* This can happen when no pointer event is assigned, like via updateDesc().
*/
if ( horIndent < -basis )
horIndent = -basis;
}
let offsetTop = 0,
offsetTopFlip = 0,
offsetLeft = 0;
// If there's a super-wrap, make everything relative from the top-left corner to the superWrap, instead of the hoveritem.
if ( superWrapRect ) {
// This is basically hoverItemWrapRect.offsetTop/offsetLeft but then with subpixel calculations.
offsetTop = hoverItemWrapRect.top - superWrapRect.top;
offsetLeft = hoverItemWrapRect.left - superWrapRect.left;
// If the text is smaller than the super wrap, center the textwrap over the hoveritem instead of the superwrap.
// This will prevent it being offset too far too the left relative to the hoveritem.
if ( textWrapWidth < superWrapRect.width )
horIndent += offsetLeft;
// If the tooltip is flipped, we need to find the relative bottom, which is what we already have.
offsetTopFlip = offsetTop;
// For regular tooltips, we need to find and subtract the relative bottom for accurate positioning.
offsetTop -= superWrapRect.height - hoverItemWrapRect.height;
}
tooltip.style.left = `${horIndent}px`; // This is calculated not to overflow.
tooltip.dataset.adjust = horIndent - offsetLeft; // This is relative to .left
// Finally, see if the tooltip overflows top or bottom. We need to do this last as the tooltip may be squashed upward.
// arrow is 8 high, add that to the total height.
const tooltipHeight = element.offsetHeight + 8;
if ( boundaryTop > ( tooltip.getBoundingClientRect().top - tooltipHeight ) ) {
tooltip.classList.add( 'tsf-tooltip-down' );
tooltip.style.top = `${tooltipHeight + offsetTopFlip}px`;
} else {
tooltip.style.bottom = `${tooltipHeight - offsetTop}px`;
}
return true;
}
/**
* Outputs tooltip.
*
* @since 3.1.0
* @since 4.0.0 1. Tooltips are now prepended, instead of appended--so they no longer break the order of flow.
* Careful, however, as some CSS queries may be subjected differently.
* 2. Now calculates up/down overflow at the end, so it accounts for squashing and stretching.
* @since 4.2.0 1. Is now asynchronous.
* 2. Now returns boolean whether the tooltip was entered successfully.
* 3. Now removes all other tooltips. Only one may prevail!
* @access public
*
* @function
* @param {event?} event Optional. The current mouse/touch event to center
* tooltip position for to make it seem more natural.
* @param {Element} element The element to add the tooltip to.
* @param {string} desc The tooltip, may contain renderable HTML.
* @return {Promise<Boolean>} True on success, false otherwise.
*/
const doTooltip = ( event, element, desc ) => {
// Backward compatibility for jQuery vs ES.
if ( element?.[0] )
element = element[0];
// Remove old tooltips, if any.
for ( const element of document.querySelectorAll( ttSelectors.base ) ) {
removeTooltip( element );
_events( element ).unset();
}
if ( ! desc.length ) return false;
return _renderTooltip( event, element, desc );
}
/**
* Adds tooltip boundaries.
*
* @since 3.1.0
* @access public
*
* @function
* @param {!jQuery|Element|string} element The jQuery element, DOM Element or query selector.
*/
const addBoundary = element => { element instanceof Element && element.classList.add( ttNames.boundary ) };
/**
* Removes the description balloon and arrow from element.
*
* @since 3.1.0
* @since 4.1.0 Now also clears the data of the tooltip.
* @access public
*
* @function
* @param {!jQuery|Element|string} element
*/
const removeTooltip = element => {
// Backward compatibility for jQuery vs ES.
if ( element?.[0] )
element = element[0];
if ( element instanceof HTMLElement ) {
delete element.dataset.hasTooltip;
_clickLocker( element ).release();
}
const toolTip = getTooltip( element );
toolTip?.parentNode.removeChild( toolTip );
}
/**
* Returns the containing tooltip, if input element isn't already a tooltip.
*
* @since 3.1.0
* @since 4.2.0 Now returns a `HTMLElement` instead of a `jQuery.Element`.
* @access public
*
* @function
* @param {!jQuery|Element|string} element
* @return {(Element|undefined)}
*/
const getTooltip = element => {
// Backward compatibility for jQuery vs ES.
if ( element?.[0] )
element = element[0];
return element?.classList.contains( ttNames.base )
? element
: element?.querySelector( ttSelectors.base );
}
let _debounceTriggerReset = void 0;
/**
* Triggers tooltip reset.
* This takes .5ms via the event handler thread, feel free to use it whenever.
*
* @since 3.1.0
* @since 4.2.0 Added debouncing.
* @access public
*
* @function
*/
const triggerReset = () => {
clearTimeout( _debounceTriggerReset );
_debounceTriggerReset = setTimeout(
() => window.dispatchEvent( new CustomEvent( 'tsf-tooltip-reset' ) ),
100 // Magic number. Low enough not to cause annoyances, high enough not to cause lag.
);
}
/**
* Triggers active tooltip update.
*
* @since 3.1.0
* @access public
*
* @function
* @param {Element|NodeList} element
*/
const triggerUpdate = element => {
if ( ! element || ! ( element instanceof Element ) )
element = document.querySelectorAll( ttSelectors.item );
if ( ! element ) return;
const updateEvent = new CustomEvent( 'tsf-tooltip-update' );
if ( element instanceof Element ) {
element.dispatchEvent( updateEvent );
} else if ( element instanceof Nodelist ) {
element.forEach( el => el.dispatchEvent( updateEvent ) );
}
}
return Object.assign( {
/**
* Initialises all aspects of the scripts.
* You shouldn't call this.
*
* @since 3.1.0
* @access protected
*
* @function
*/
load: () => {
document.body.addEventListener( 'tsf-ready', _initToolTips );
}
}, {
/**
* Copies internal public functions to tsfTT for public access.
* Don't overwrite these.
*
* @since 3.1.0
* @access public
*/
doTooltip,
removeTooltip,
getTooltip,
addBoundary,
triggerReset,
triggerUpdate,
} );
}();
window.tsfTT.load();