const roundByDPR = (value) => { const dpr = window.devicePixelRatio || 1; return Math.round(value * dpr) / dpr; }; export const createTooltip = ($el, content, options) => { options = { appendTo: document.body, arrow: true, delay: [0, 0], duration: [0, 0], easing: ['linear', 'linear'], hideOnClick: true, interactive: true, offset: [0, 8], placement: 'top', theme: 'light', trigger: 'mouseenter', virtialReference: undefined, zIndex: '', ...options, }; if ($el.dataset.tooltipArrow !== undefined) { options.arrow = $el.dataset.tooltipArrow === 'true'; } if ($el.dataset.tooltipHideOnClick !== undefined) { switch ($el.dataset.tooltipHideOnClick) { case 'all': case 'toggle': { options.hideOnClick = $el.dataset.tooltipHideOnClick; break; } default: { options.hideOnClick = $el.dataset.tooltipHideOnClick === 'true'; } } } if ($el.dataset.tooltipInteractive !== undefined) { options.interactive = $el.dataset.tooltipInteractive === 'true'; } if ($el.dataset.tooltipOffset !== undefined) { options.offset = JSON.parse($el.dataset.tooltipOffset); } if ($el.dataset.tooltipPlacement !== undefined) { options.placement = $el.dataset.tooltipPlacement; } if ($el.dataset.tooltipTheme !== undefined) { options.theme = $el.dataset.tooltipTheme; } if ($el.dataset.tooltipTrigger !== undefined) { options.trigger = $el.dataset.tooltipTrigger; } if ($el.dataset.tooltipZIndex !== undefined) { options.zIndex = $el.dataset.tooltipZIndex; } let showTimeout; let hideTimeout; let autoUpdateCleanup = new Function(); const listeners = []; $el._tooltip = { _options: options, isVisible: false, setContent(updatedContent) { if (updatedContent !== undefined) { content = updatedContent; } if ($el._tooltip.$tooltip) { const $container = $el._tooltip.$tooltip.querySelector('.tooltip__container'); if (content instanceof HTMLElement) { $container.innerHTML = ''; $container.append(content); } else { $container.innerHTML = content; } } }, async updateOptions(updatedOptions = {}) { for (const [name, value] of Object.entries(updatedOptions)) { options[name] = value; } if (updatedOptions.arrow !== undefined && $el._tooltip.$tooltip) { if (options.arrow) { if (!$el._tooltip.$tooltip.querySelector('.tooltip__arrow')) { $el._tooltip.$tooltip.insertAdjacentHTML( 'afterbegin', `
`, ); } } else { $el._tooltip.$tooltip.querySelector('.tooltip__arrow')?.remove(); } } if (updatedOptions.interactive !== undefined) { for (const { el, event, listener } of listeners) { el.removeEventListener(event, listener); } registerListeners(); if ($el._tooltip.$tooltip) { if (options.interactive) { if (!$el._tooltip.$tooltip.querySelector('.tooltip__interactive-helper')) { $el._tooltip.$tooltip.insertAdjacentHTML( 'afterbegin', `
`, ); } } else { $el._tooltip.$tooltip.querySelector('.tooltip__interactive-helper')?.remove(); } } } if (updatedOptions.theme !== undefined && $el._tooltip.$tooltip) { const classIndex = [...$el._tooltip.$tooltip.classList].findIndex((className) => className.startsWith('tooltip_theme_'), ); $el._tooltip.$tooltip.classList.replace( $el._tooltip.$tooltip.classList[classIndex], `tooltip_theme_${options.theme}`, ); } if (updatedOptions.zIndex !== undefined && $el._tooltip.$tooltip) { Object.assign($el._tooltip.$tooltip.style, { zIndex: options.zIndex }); } if ($el._tooltip.$tooltip) { await $el._tooltip.updatePosition(); } }, destroy() { $el._tooltip.$tooltip?.remove(); autoUpdateCleanup(); for (const { el, event, listener } of listeners) { el.removeEventListener(event, listener); } delete $el._tooltip; }, }; if (options.onCreate) { options.onCreate($el._tooltip); } $el._tooltip.show = async () => { clearTimeout(hideTimeout); if (!$el._tooltip.$tooltip) { const { computePosition, offset, flip, shift, arrow } = await import('@floating-ui/dom'); $el._tooltip.$tooltip = document.createElement('div'); $el._tooltip.updatePosition = async () => { const $arrow = $el._tooltip.$tooltip.querySelector('.tooltip__arrow'); const { x, y, placement, middlewareData } = await computePosition( options.virtialReference ?? $el, $el._tooltip.$tooltip, { placement: options.placement, middleware: [ offset({ mainAxis: options.offset[1], crossAxis: options.offset[0], }), flip(), shift(), arrow({ element: $arrow, padding: Number.parseInt( getComputedStyle($el._tooltip.$tooltip.querySelector('.tooltip__container'))[ 'border-radius' ], ), }), ], }, ); Object.assign($el._tooltip.$tooltip.style, { transform: `translate(${roundByDPR(x)}px, ${roundByDPR(y)}px)`, }); const side = placement.split('-')[0]; const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right', }[side]; $el._tooltip.$tooltip.classList.remove( 'tooltip_side_top', 'tooltip_side_bottom', 'tooltip_side_left', 'tooltip_side_right', ); $el._tooltip.$tooltip.classList.add(`tooltip_side_${side}`); const $interactive = $el._tooltip.$tooltip.querySelector('.tooltip__interactive-helper'); if ($interactive) { Object.assign($interactive.style, { width: side === 'left' || side === 'right' ? `${options.offset[1]}px` : '100%', height: side === 'top' || side === 'bottom' ? `${options.offset[1]}px` : '100%', top: '', bottom: '', left: '', right: '', [staticSide]: `-${options.offset[1]}px`, }); } if ($arrow && middlewareData.arrow) { const { x, y } = middlewareData.arrow; Object.assign($arrow.style, { left: x === undefined ? '' : `${x}px`, top: y === undefined ? '' : `${y}px`, right: '', bottom: '', [staticSide]: `-${$arrow.offsetWidth / 2}px`, }); } }; $el._tooltip.$tooltip.classList.add('tooltip', `tooltip_theme_${options.theme}`); Object.assign($el._tooltip.$tooltip.style, { zIndex: options.zIndex }); $el._tooltip.$tooltip.innerHTML = ` ${ options.interactive ? `
` : '' } ${ options.arrow ? `
` : '' }
`; $el._tooltip.setContent(); if (options.onMount) { options.onMount($el._tooltip); } } const { autoUpdate } = await import('@floating-ui/dom'); showTimeout = setTimeout(async () => { if (!$el._tooltip.isVisible) { options.appendTo.append($el._tooltip.$tooltip); $el._tooltip.isVisible = true; autoUpdateCleanup = autoUpdate($el, $el._tooltip.$tooltip, $el._tooltip.updatePosition); if (options.onShow) { options.onShow($el._tooltip); } await $el._tooltip.$tooltip.animate([{ opacity: 0 }, { opacity: 1 }], { duration: options.duration[1], easing: options.easing[1], }).finished; if (options.onShown) { options.onShown($el._tooltip); } } }, options.delay[0]); }; $el._tooltip.hide = () => { clearTimeout(showTimeout); hideTimeout = setTimeout(async () => { if ($el._tooltip.isVisible) { if (options.onHide) { options.onHide($el._tooltip); } await $el._tooltip.$tooltip.animate([{ opacity: 1 }, { opacity: 0 }], { duration: options.duration[1], easing: options.easing[1], }).finished; $el._tooltip.$tooltip.remove(); $el._tooltip.isVisible = false; autoUpdateCleanup(); if (options.onHidden) { options.onHidden($el._tooltip); } } }, options.delay[1]); }; const hideOnClickListener = ({ target }) => { if ( $el._tooltip.isVisible && (options.hideOnClick === 'all' || $el.contains(target) || (options.hideOnClick !== 'toggle' && !$el._tooltip.$tooltip.contains(target))) ) { document.body.removeEventListener('click', hideOnClickListener); $el._tooltip.hide(); } }; const clickListener = () => { if (!$el._tooltip.isVisible) { $el._tooltip.show(); if (options.hideOnClick) { setTimeout(() => { document.body.addEventListener('click', hideOnClickListener); listeners.push({ el: document.body, event: 'click', listener: hideOnClickListener, }); }); } } }; const mouseEnterListener = () => { $el._tooltip.show(); }; const mouseLeaveListener = ({ relatedTarget }) => { if (options.interactive) { if ($el._tooltip.$tooltip.contains(relatedTarget)) { $el._tooltip.$tooltip.addEventListener( 'mouseleave', ({ relatedTarget }) => { if (!$el.contains(relatedTarget)) { $el._tooltip.hide(); } }, { once: true, }, ); } else { $el._tooltip.hide(); } } else { $el._tooltip.hide(); } }; const registerListeners = () => { for (const trigger of options.trigger.split(' ')) { switch (trigger) { case 'mouseenter': { $el.addEventListener('mouseenter', mouseEnterListener); listeners.push({ el: $el, event: 'mouseenter', listener: mouseEnterListener, }); $el.addEventListener('mouseleave', mouseLeaveListener); listeners.push({ el: $el, event: 'mouseleave', listener: mouseLeaveListener, }); break; } case 'click': { $el.addEventListener('click', clickListener); listeners.push({ el: $el, event: 'click', listener: clickListener, }); if (!options.interactive) { $el.addEventListener('mouseleave', mouseLeaveListener); listeners.push({ el: $el, event: 'mouseleave', listener: mouseLeaveListener, }); } break; } case 'manual': { if (options.hideOnClick) { document.body.addEventListener('click', hideOnClickListener); listeners.push({ el: document.body, event: 'click', listener: hideOnClickListener, }); } break; } } } }; registerListeners(); };