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;