import _ from 'lodash' import { onClickOutside } from '@baserow/modules/core/utils/dom' /** * helper function to extract options from element * * If binding provides .arg, the .arg property should be an object: * * {duration: Number, contentIsHtml: Bool, contentClasses: String} * * @param el * @param binding directive binding value * @returns {{duration: number, contentClasses: (*|null), value, contentType: (*|string)}} */ const getOptions = (el, binding) => { // defaults const tooltipOptions = { duration: 0, contentIsHtml: false, value: null, contentClasses: '', } if (binding.arg && _.isObject(binding.arg)) { _.merge(tooltipOptions, binding.arg) } tooltipOptions.value = binding.value return tooltipOptions } // make sure only one tooltip is visible let currentTooltip = null const switchToTooltip = (el) => { if (currentTooltip != null) { currentTooltip.tooltipClose() } currentTooltip = el } /** * This is a very simple and fast tooltip directive. It will add the binding value as * tooltip content. The tooltip only shows if there is a value. * * tooltip directive can be customized with arg, where arg is an object: * { * // duration number of seconds the tooltip should be visible after pointer * // leaves i icon or tooltip content * duration: Number, * // contentIsHtml informs the value is html and should not be escaped (sets .innerHtml directly) * contentIsHtml: Bool, * // contentClasses is a string with additional css classes for tooltip content container * contentClasses: String * } * * <i v-tooltip:[configObject]="someValue"/> */ export default { /** * If there is a value and the tooltip has not yet been initialized we can add the * mouse events to show and hide the tooltip. * */ initialize(el, binding) { el.tooltipOptions = getOptions(el, binding) el.onClickOutsideCallback = null el.updatePositionEvent = () => { const rect = el.getBoundingClientRect() const position = el.getAttribute('tooltip-position') || 'bottom' if (position === 'top') { const rectTooltip = el.tooltipElement.getBoundingClientRect() el.tooltipElement.style.top = rect.top - 2 - rectTooltip.height + 'px' } else { el.tooltipElement.style.top = rect.bottom + 4 + 'px' } const width = rect.right - rect.left el.tooltipElement.style.left = rect.left + width / 2 + 'px' } el.removeTimeout = () => { if (el.tooltipTimeout) { clearTimeout(el.tooltipTimeout) } el.tooltipTimeout = null } el.tooltipMouseEnterEvent = () => { switchToTooltip(el) const position = el.getAttribute('tooltip-position') || 'bottom' const hide = el.getAttribute('hide-tooltip') if (hide) { return } if (!el.tooltipElement) { el.tooltipElement = document.createElement('div') const classes = ['tooltip', 'tooltip--body', 'tooltip--center'] if (position === 'top') { classes.push('tooltip--top') } el.tooltipElement.className = classes.join(' ') document.body.insertBefore(el.tooltipElement, document.body.firstChild) el.tooltipContentElement = document.createElement('div') el.tooltipElement.appendChild(el.tooltipContentElement) } if (el.tooltipOptions.contentIsHtml) { el.tooltipContentElement.innerHTML = el.tooltipOptions.value } else { el.tooltipContentElement.textContent = el.tooltipOptions.value } // additional css classes for content container const contentClass = ['tooltip__content'] if (el.tooltipOptions.contentClasses) { contentClass.push(el.tooltipOptions.contentClasses) } el.tooltipContentElement.className = contentClass.join(' ') el.updatePositionEvent() // we just entered, so we don't want any previously set timeout to close // the tooltip content el.removeTimeout() // make tooltip content preserved if pointer hovers el.tooltipContentElement.addEventListener('mouseenter', el.removeTimeout) el.tooltipContentElement.addEventListener( 'mouseleave', el.tooltipMoveLeaveEvent ) // When the user scrolls or resizes the window it could be possible that the // element where the tooltip is anchored to has moved, so then the position // needs to be updated. We only want to do this when the tooltip is visible. window.addEventListener('scroll', el.updatePositionEvent, true) window.addEventListener('resize', el.updatePositionEvent) // refresh the callback - old callback should be removed because old instance // is may be longer present, and window object may still have click event handlers // that check if the pointer is in or out of tooltip content element (which itself // is long time gone) el.removeTooltipOutsideClickCallback() el.onClickOutsideCallback = onClickOutside( el.tooltipContentElement, el.tooltipClose ) } /** * queue a close tooltip action. * * This can be called multiple times. Each call will postpone tooltipClose() call. * This way user can hover in and hover out several times and the tooltip still be * visible, if duration is > 0. */ el.tooltipMoveLeaveEvent = () => { // we should remove any pending timeout before setting new one, because timeout // should be counted from the last mouse leave event. el.removeTimeout() el.tooltipTimeout = setTimeout( el.tooltipClose, // timeout from caller is in seconds. remember to convert to mseconds el.tooltipOptions.duration * 1000 ) } el.removeTooltipOutsideClickCallback = () => { if (el.onClickOutsideCallback) { el.onClickOutsideCallback() el.onClickOutsideCallback = null } } /** * actually closing the tooltip here */ el.tooltipClose = () => { // cleanup actions: remove window handlers set with onClickOutside() el.removeTooltipOutsideClickCallback() if (el.tooltipElement) { el.tooltipElement.parentNode.removeChild(el.tooltipElement) el.tooltipElement = null el.tooltipContentElement = null } window.removeEventListener('scroll', el.updatePositionEvent, true) window.removeEventListener('resize', el.updatePositionEvent) el.removeTimeout() } // those event listeners should be bind all the time to the el element el.addEventListener('mouseenter', el.tooltipMouseEnterEvent) el.addEventListener('mouseleave', el.tooltipMoveLeaveEvent) }, /** * If there isn't a value or if the directive is unbinded the tooltipElement can * be destroyed if it wasn't already and all the events can be removed. */ terminate(el) { if (el.tooltipElement && el.tooltipElement.parentNode) { el.tooltipElement.parentNode.removeChild(el.tooltipElement) } el.tooltipElement = null el.tooltipContentElement = null el.removeEventListener('mouseenter', el.tooltipMouseEnterEvent) el.removeEventListener('mouseleave', el.tooltipMoveLeaveEvent) window.removeEventListener('scroll', el.updatePositionEvent, true) window.removeEventListener('resize', el.updatePositionEvent) }, bind(el, binding) { el.tooltipElement = null el.tooltipContentElement = null binding.def.update(el, binding) }, update(el, binding) { const { value } = binding if (!!value && el.tooltipElement) { el.tooltipOptions = getOptions(el, binding) } else if (!!value && el.tooltipElement === null) { binding.def.initialize(el, binding) } else if (!value) { binding.def.terminate(el) } }, unbind(el, binding) { binding.def.terminate(el) }, }