import populateElement from '../utils/populateElement';
/**
* Module for showing a tool tip when user hovers over an info button
*
* @module ToolTip
* @prop {string} selector - Selector for buttons that trigger tool tip.
* @prop {object} states - Set of flags that can be used to toggle various tool tip states.
* @prop {string} template - HTML template of tool tip, supporting a title, main body content, link.
*/
const ToolTip = {
selector: '[data-tip]',
buttonSelector: '.c-toolTip__button',
states: {
main: 'c-toolTip',
mobile: 'c-toolTip--mobile',
desktop: 'c-toolTip--desktop',
active: 'c-toolTip--active',
animateIn: 'is-faded-in',
animateOut: 'is-faded-out',
right: 'c-toolTip--right',
flipped: 'c-toolTip--flipped',
},
template: `
<div class="c-toolTip__pointer"></div>
<div class="c-toolTip__inner">
<div class="c-toolTip__title" data-map="tipTitle"></div>
<div class="c-toolTip__content" data-map="tipContent"></div>
<a class="c-toolTip__link" data-map="tipLink" href="" target="_blank">
<span class="c-toolTip__linkLabel" data-map="tipLinkLabel"></span>
</a>
</div>
<div class="c-toolTip__button">
<svg class="c-toolTip__icon c-toolTip__icon--close"><use xlink:href="/wp-content/themes/shaw-globalnews/assets/dist/icons/out/symbol/svg/sprite.symbol.svg?#close"></svg>
</div>
`,
/**
* Set up tool tips for designated info buttons
* and set up mouse event listeners for show / hiding tool tips
*
* @method init
*/
init() {
const $targets = document.querySelectorAll( this.selector );
[].forEach.call( $targets, ( $elem, index ) => {
this.initItem( $elem, index );
});
},
/**
* Set up tool tips for designated a specific button
*
* @method initItem
* @param {element} $elem - button element
* @param {string|number} itemId - id for identifying the button element
* @param {element} $context - optional context for selecting mobile container for tool tip
*/
initItem( $elem, itemId, $context = document ) {
const id = `${this.states.main}${itemId}`;
$elem.dataset.tipId = id; /* eslint-disable-line no-param-reassign */
let $tip = this.addTip( $elem, $elem.dataset, id, this.states.desktop, $context );
if ( $elem.dataset.tipMobileContainer ) {
const $container = $context.querySelector( $elem.dataset.tipMobileContainer );
$tip = this.addTip( $container, $elem.dataset, id, this.states.mobile );
$container.dataset.tipId = id;
$container.addEventListener( 'mouseleave', evt => this.handleMouseLeave( evt ) );
if ( $tip ) {
const $button = $tip.querySelector( this.buttonSelector );
$button.dataset.tipId = id;
$button.addEventListener( 'click', evt => this.handleMouseLeave( evt ) );
}
}
$elem.addEventListener( 'mouseover', evt => this.handleMouseEnter( evt ) );
$elem.addEventListener( 'mouseleave', evt => this.handleMouseLeave( evt ) );
},
/**
* Create a tool tip displaying provided data
* and append the tool tip to the designated container element
*
* @method addTip
* @param {element} $container - container element
* @param {object} data - data to be populated into tool tip
* @param {string} id - tool tip id
* @param {string} customCss - custom css class
* @param {element} $boundary - optional. Apply custom css to ensure tool tip is within bound
*/
addTip( $container, data, id, customCss, $boundary = null ) {
if ( ! $container ) {
return false;
}
let $tip = document.createElement( 'div' );
$tip.classList.add( this.states.main );
$tip.dataset.tipId = id;
let selector = `.${this.states.main}`;
if ( customCss ) {
$tip.classList.add( customCss );
selector = `${selector}.${customCss}`;
}
$tip.innerHTML = this.template;
populateElement( $tip, data );
$container.appendChild( $tip );
// Get the $tip element added to $container
$tip = $container.querySelector( selector );
// Set custom css if $tip is out of bound
if ( $boundary && document !== $boundary ) {
const rect = $tip.getBoundingClientRect();
const boundaryRect = $boundary.getBoundingClientRect();
const right = boundaryRect.x + boundaryRect.width;
const bottom = boundaryRect.y + boundaryRect.height;
if ( rect.x + rect.width > right - 20 ) {
$tip.classList.add( this.states.right );
}
if ( rect.y + rect.height > bottom - 20 ) {
$tip.classList.add( this.states.flipped );
}
}
return $tip;
},
/**
* Show tool tip on mouse enter
*
* @method handleMouseEnter
*/
handleMouseEnter( evt ) {
const $tips = document.querySelectorAll( `.${this.states.main}[data-tip-id="${evt.currentTarget.dataset.tipId}"]` );
[].forEach.call( $tips, ( $tip ) => {
$tip.classList.add( this.states.active );
$tip.classList.add( this.states.animateIn );
});
},
/**
* Hide tool tip on mouse leave
*
* @method handleMouseLeave
*/
handleMouseLeave( evt ) {
const $tips = document.querySelectorAll( `.${this.states.main}[data-tip-id="${evt.currentTarget.dataset.tipId}"]` );
let mobileTipVisible = false;
[].forEach.call( $tips, ( $tip ) => {
if ( $tip.classList.contains( this.states.mobile ) ) {
const style = window.getComputedStyle( $tip );
mobileTipVisible = 'block' === style.getPropertyValue( 'display' );
}
// If mobile view of tool tip is visible,
// only hide the tip when mouse leave triggered by mobile container
if ( ! evt.currentTarget.dataset.tipMobileContainer || ! mobileTipVisible ) {
$tip.classList.remove( this.states.active );
$tip.classList.remove( this.states.animateIn );
$tip.classList.add( this.states.animateOut );
}
});
},
};
export default ToolTip;