const tooltip = (element: HTMLElement) => { let tooltipDiv: HTMLDivElement | null = null; const offset = 10; const tooltipTransitionTime = 200; const tooltipHideDelay = 10; const debounceDelay = 100; let hideTimeout: number | null = null; let debounceTimer: number | null = null; if (element.dataset.tooltipDisable === 'true') return; const createTooltip = () => { if (!tooltipDiv) { tooltipDiv = document.createElement('div'); tooltipDiv.style.position = 'absolute'; tooltipDiv.style.zIndex = '1000'; tooltipDiv.style.opacity = '0'; tooltipDiv.style.transition = `opacity ${tooltipTransitionTime}ms ease-in-out, top 0.3s ease, left 0.3s ease`; tooltipDiv.style.pointerEvents = 'none'; tooltipDiv.style.whiteSpace = 'nowrap'; tooltipDiv.style.zIndex = '1000'; tooltipDiv.classList.add('card'); tooltipDiv.classList.add('card-small'); document.body.appendChild(tooltipDiv); } }; const updateTooltipPosition = (x: number, y: number) => { if (tooltipDiv) { const tooltipPin = element.dataset.tooltippin; if (tooltipPin) { const pinnedElement = document.getElementById(tooltipPin); if (pinnedElement) { const rect = pinnedElement.getBoundingClientRect(); const tooltipWidth = tooltipDiv.offsetWidth; const tooltipHeight = tooltipDiv.offsetHeight; let top = rect.top - tooltipHeight - offset; let left = rect.left + rect.width / 2 - tooltipWidth / 2; if (left < 0) left = offset; if (left + tooltipWidth > window.innerWidth) left = window.innerWidth - tooltipWidth - offset; if (top < 0) top = rect.top + rect.height + offset; tooltipDiv.style.left = `${left}px`; tooltipDiv.style.top = `${top + window.scrollY}px`; return; } } const tooltipWidth = tooltipDiv.offsetWidth; const tooltipHeight = tooltipDiv.offsetHeight; let top = y - tooltipHeight - offset; let left = x - tooltipWidth / 2; if (left < 0) left = offset; if (left + tooltipWidth > window.innerWidth) left = window.innerWidth - tooltipWidth - offset; if (top < 0) top = y + offset; tooltipDiv.style.left = `${left}px`; tooltipDiv.style.top = `${top}px`; } }; const showTooltip = (content: string, x: number, y: number) => { if (hideTimeout !== null) { clearTimeout(hideTimeout); hideTimeout = null; } createTooltip(); if (tooltipDiv) { tooltipDiv.innerHTML = content.replace(/\n/g, '
'); updateTooltipPosition(x, y); setTimeout(() => { if (tooltipDiv) { tooltipDiv.style.opacity = '1'; } }, 10); } }; const hideTooltip = () => { setTimeout(() => { if (tooltipDiv) { tooltipDiv.style.opacity = '0'; hideTimeout = window.setTimeout(() => { if (tooltipDiv) { document.body.removeChild(tooltipDiv); tooltipDiv = null; } }, tooltipTransitionTime); } }, tooltipHideDelay); }; const handleMouseEnter = (event: MouseEvent) => { const title = element.getAttribute('title'); if (title) { element.removeAttribute('title'); if (hideTimeout !== null) { clearTimeout(hideTimeout); hideTimeout = null; } if (!tooltipDiv) { showTooltip(title, event.pageX, event.pageY); } } }; const handleMouseMove = (event: MouseEvent) => { if (debounceTimer !== null) clearTimeout(debounceTimer); debounceTimer = window.setTimeout(() => { if (tooltipDiv && tooltipDiv.style.opacity === '1') updateTooltipPosition(event.pageX, event.pageY); }, debounceDelay); }; const handleMouseLeave = () => { element.setAttribute( 'title', tooltipDiv ? tooltipDiv.innerHTML?.replace(/
/g, '\n') || '' : '' ); hideTooltip(); }; element.addEventListener('mouseenter', handleMouseEnter); element.addEventListener('mousemove', handleMouseMove); element.addEventListener('mouseleave', handleMouseLeave); return { destroy() { element.removeEventListener('mouseenter', handleMouseEnter); element.removeEventListener('mousemove', handleMouseMove); element.removeEventListener('mouseleave', handleMouseLeave); if (hideTimeout !== null) clearTimeout(hideTimeout); if (debounceTimer !== null) clearTimeout(debounceTimer); if (tooltipDiv && document.body.contains(tooltipDiv)) { document.body.removeChild(tooltipDiv); tooltipDiv = null; } } }; }; export default tooltip;