//============================================================================== // Copyright 2016-2025 Moraware, Inc. // // Purpose: Custom tooltips, configurable with a custom arrow and following // Moraware style guides. // Data Attributes: // MWTooltip - Trigger for showing a tooltip, takes the text to // display // mwtooltipdurationseconds - how long to keep the tooltip open // for. Defaults to 2 seconds. // mwignoreparentforposition - normally we calculate the tooltip // position using all of the parent offsets to account for // scrolling to make the sure the tooltip is in the correct // place. In some cases, we want to ignore that and use the // actual location of the element. // mwshowonclick - set the tooltip to show when the tipped // element is clicked. Useful for icon style help elements. // //============================================================================== /*global rsPageXOffset isDialogShowing checkAttribute getEventElement pageHeight pageWidth posTop posLeft getObj mjtPageOffset htmlMultilineEncode */ //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ var MWToolTipper = function() { var TOOLTIP_HOVER_TIMEOUT_MS = 250, TOOLTIP_ATTR_NAME = "data-MWTooltip", TOOLTIP_ELEMENT_ID = 'mwToolTip', IS_TOOLTIP_TARGET_ATTR_NAME = "data-MWIsTipTarget", TOOLTIP_ARROW_OBJECT_ID = 'mwToolTipArrow', TOOLTIP_HOVER_DURATION_SECONDS = 2, m_tempIgnoreMouseMove = 0, m_timerIgnoreMouse, m_currTippedElement, m_tooltipElement, m_timerLinkTooltip, m_timerHideTooltip, m_tooltipArrowElement; //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvUnlinkCurrentTooltip() { if(m_timerLinkTooltip) { clearTimeout(m_timerLinkTooltip); m_timerLinkTooltip = null; } if(m_timerHideTooltip) { clearTimeout(m_timerHideTooltip); m_timerHideTooltip = null; } if(m_currTippedElement) { m_currTippedElement.removeAttribute(IS_TOOLTIP_TARGET_ATTR_NAME); m_tooltipElement.style.display = 'none'; m_tooltipArrowElement.style.display = 'none'; } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvOnMouseOutToolTip(event) { var e = event.toElement || event.relatedTarget; if(!e) { // Hide the tooltip if the mouseout occurs outside of the browser // window pvUnlinkCurrentTooltip(); } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvHasFixedPositionParent(element_) { while(element_ && element_.style) { if(element_.style.position === 'fixed' || (window.getComputedStyle && window.getComputedStyle(element_).position === 'fixed')) { return true; } element_ = element_.parentNode; } return false; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pbHideTooltip() { if(m_tooltipElement && m_tooltipElement.style.display !== 'none') { m_tooltipElement.style.display = 'none'; var objArrow = getObj(TOOLTIP_ARROW_OBJECT_ID); if(objArrow && objArrow.style.display !== 'none') { objArrow.style.display = 'none'; } } pvUnlinkCurrentTooltip(); } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvCreateTooltipIfNecessary() { if(!m_tooltipElement) { m_tooltipElement = document.createElement('div'); m_tooltipElement.id = TOOLTIP_ELEMENT_ID; m_tooltipElement.style.display = 'none'; m_tooltipElement.className = 'toolTip'; document.body.appendChild(m_tooltipElement); m_tooltipArrowElement = document.createElement('div'); m_tooltipArrowElement.id = TOOLTIP_ARROW_OBJECT_ID; m_tooltipArrowElement.style.display = 'none'; m_tooltipArrowElement.className = 'toolTipArrow'; document.body.appendChild(m_tooltipArrowElement); } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvResetHideTimer() { var tmpTooltipHoverDurationSeconds = TOOLTIP_HOVER_DURATION_SECONDS; if (m_timerHideTooltip) { clearTimeout(m_timerHideTooltip); } if (m_currTippedElement) { if (m_currTippedElement.dataset.mwtooltipdurationseconds) { var strHoverDurationSeconds = m_currTippedElement.dataset.mwtooltipdurationseconds; //Only override the duration if it is a number //and above 0 var tmpHoverDurationSeconds = parseInt(strHoverDurationSeconds, 10); if (tmpHoverDurationSeconds > 0) { tmpTooltipHoverDurationSeconds = tmpHoverDurationSeconds; } } } m_timerHideTooltip = setTimeout(pbHideTooltip, tmpTooltipHoverDurationSeconds * 1000); } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvElementOrAncestorIsGoneOrDisplayNone(element_) { var rc = false, tmpNonNullElementIsNeitherBodyNorHtmlElement = element_ && element_.nodeName !== 'BODY' && element_.nodeName !== 'HTML'; if(tmpNonNullElementIsNeitherBodyNorHtmlElement) { if(!document.body.contains(element_)) { rc = true; } else if(element_.style.display === 'none') { rc = true; } else { rc = pvElementOrAncestorIsGoneOrDisplayNone( element_.parentNode); } } return rc; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvLinkTooltip(tippedElement_) { if(!tippedElement_ || pvElementOrAncestorIsGoneOrDisplayNone(tippedElement_)) { return; } var strTipValue = tippedElement_.getAttribute(TOOLTIP_ATTR_NAME), tmpIgnoreParentForPosition = tippedElement_.dataset.mwignoreparentforposition; tippedElement_.setAttribute(IS_TOOLTIP_TARGET_ATTR_NAME, '1'); m_currTippedElement = tippedElement_; pvCreateTooltipIfNecessary(); m_tooltipElement.innerHTML = htmlMultilineEncode(strTipValue); m_tooltipElement.style.display = 'block'; m_tooltipElement.style.left = 0; m_tooltipElement.style.top = 0; m_tooltipArrowElement.style.top = 0; m_tooltipArrowElement.style.left = 0; m_tooltipArrowElement.style.display = 'block'; var tmpTop, tmpLeft, tmpTargetPosTop = tmpIgnoreParentForPosition ? tippedElement_.getBoundingClientRect().top : posTop(tippedElement_, true/*accountForScrollTop_*/), // tippedElement_.getBoundingClientRect().top tmpTargetPosLeft = tmpIgnoreParentForPosition ? tippedElement_.getBoundingClientRect().left : posLeft(tippedElement_, true/*accountForScrollLeft_*/), //tippedElement_.getBoundingClientRect().left elementCX = tippedElement_.offsetWidth, elementCY = tippedElement_.offsetHeight, tipCX = m_tooltipElement.offsetWidth, tipCY = m_tooltipElement.offsetHeight, tmpPageCX = pageWidth(), tmpPageCY = pageHeight(), arrPageOffset = mjtPageOffset(), tmpPageLeft = arrPageOffset[0], tmpPageRight = tmpPageLeft + tmpPageCX, tmpPageTop = arrPageOffset[1], tmpPageBottom = arrPageOffset[1] + tmpPageCY, tmpTipVOffsetAbove = 8, tmpTipVOffsetBelow = 15, tmpArrowTop, tmpArrowWidth = m_tooltipArrowElement.offsetWidth, tmpArrowLeft = Math.max( tmpTargetPosLeft, (tmpTargetPosLeft + (elementCX / 2)) - (tmpArrowWidth / 2)); var tmpTargetAncestorChain = tippedElement_; if(!isDialogShowing()) { while(tmpTargetAncestorChain && tmpTargetAncestorChain.nodeName !== 'BODY') { var cs = window.getComputedStyle(tmpTargetAncestorChain, null); if('fixed' === cs.position) { tmpTargetPosTop += tmpPageTop; tmpTargetPosLeft += tmpPageLeft; break; } tmpTargetAncestorChain = tmpTargetAncestorChain.parentNode; } } var targetHorizontalMidPoint = tmpTargetPosLeft + (elementCX / 2); tmpTop = tmpTargetPosTop + elementCY + tmpTipVOffsetBelow; tmpArrowTop = tmpTop - m_tooltipArrowElement.offsetHeight; tmpLeft = targetHorizontalMidPoint - (tipCX / 2); if(tmpLeft + tipCX > tmpPageRight - 12) { tmpLeft = tmpPageRight - 12 - tipCX; } else if(tmpLeft < tmpPageLeft) { tmpLeft = tmpPageLeft; } if(tmpTop + tipCY > tmpPageBottom) { tmpTop = tmpTargetPosTop - tipCY - tmpTipVOffsetAbove; m_tooltipArrowElement.classList.add('flipped'); tmpArrowTop = tmpTop + m_tooltipElement.offsetHeight; } else { m_tooltipArrowElement.classList.remove('flipped'); } if(pvHasFixedPositionParent(tippedElement_)) { tmpArrowLeft += rsPageXOffset(); } m_tooltipElement.style.top = tmpTop + 'px'; m_tooltipElement.style.left = tmpLeft + 'px'; m_tooltipArrowElement.style.top = tmpArrowTop + 'px'; m_tooltipArrowElement.style.left = tmpArrowLeft + 'px'; m_currTippedElement.addEventListener('mouseout', pvOnMouseOutToolTip); pvResetHideTimer(); } function pvOnPointerDown(event) { var objDirect = getEventElement(event), objMarkedElement = checkAttribute(objDirect, TOOLTIP_ATTR_NAME), strTipTarget = objMarkedElement ? objMarkedElement.getAttribute( IS_TOOLTIP_TARGET_ATTR_NAME) : 0; if (!strTipTarget) { // Either we're switching targets or we're going from target to no target. // we're clearing the current target. pvUnlinkCurrentTooltip(); if (objMarkedElement && objMarkedElement.dataset.mwshowonclick) { if (objMarkedElement) { pvLinkTooltip(objMarkedElement/*tippedElement_*/); } } } } //-------------------------------------------------------------------------- // Display a tooltip on a click event. This is used by help icons // to show information to users on mobile devices //-------------------------------------------------------------------------- function pbShowTooltipOnClick(event) { var objDirect = getEventElement(event), objMarkedElement = checkAttribute(objDirect, TOOLTIP_ATTR_NAME), strTipTarget = objMarkedElement ? objMarkedElement.getAttribute( IS_TOOLTIP_TARGET_ATTR_NAME) : 0; if (objMarkedElement) { if (!strTipTarget) { // Either we're switching targets or we're going from target to no target. // we're clearing the current target. pvUnlinkCurrentTooltip(); pvLinkTooltip(objMarkedElement/*tippedElement_*/); } //Do not allow the click event to bubble up //Clicking on an element to show a tooltip should not //also trigger a parent elements click event event.preventDefault(); return false; } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvMouseMove(event) { if(m_tempIgnoreMouseMove) { return; } var objDirect = getEventElement(event), objMarkedElement = checkAttribute(objDirect, TOOLTIP_ATTR_NAME), strTipTarget = objMarkedElement ? objMarkedElement.getAttribute( IS_TOOLTIP_TARGET_ATTR_NAME) : 0; if(!strTipTarget) { // Either we're switching targets or we're going from target to no target. // Either way, we're clearing the current target. pvUnlinkCurrentTooltip(); if(objMarkedElement) { m_timerLinkTooltip = setTimeout( function() { pvLinkTooltip(objMarkedElement/*tippedElement_*/); }, TOOLTIP_HOVER_TIMEOUT_MS); } } else if(objMarkedElement) { // We're moving over the same tooltip element, so reset the // hide timer to make sure the current tooltip stays visible. pvResetHideTimer(); } } //-------------------------------------------------------------------------- // // Try to prevent the tooltip from showing up on a (pure) touch device. // //-------------------------------------------------------------------------- function pvTouchStart() { if(m_timerIgnoreMouse) { clearTimeout(m_timerIgnoreMouse); } m_timerIgnoreMouse = setTimeout( function() { m_tempIgnoreMouseMove = 0; }, 500); m_tempIgnoreMouseMove = 1; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvSynchInitToolTipper() { if(document.addEventListener) { document.addEventListener('mousemove', pvMouseMove); document.addEventListener('touchstart', pvTouchStart, true); document.addEventListener('pointerdown', pvOnPointerDown); } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvInitToolTipper() { if(window.addEventListener) { window.addEventListener('load', pvSynchInitToolTipper); } } // Initialize the tool tip before returning pvInitToolTipper(); //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- return { hideTooltip: pbHideTooltip, showTooltipOnClick: pbShowTooltipOnClick }; }(); //------------------------------------------------------------------------------ // // TODO: Replace the use of the "mwHideTooltip()" function with direct calls to // "MWToolTipper.hideTooltip()". Once this method has been substituted in // everywhere else in the code, the definition of this call-through // function can be removed. // //------------------------------------------------------------------------------ // eslint-disable-next-line no-unused-vars function mwHideTooltip() { MWToolTipper.hideTooltip(); }