/** * tooltip.js * * This script uses FloatingUI to create modern, styled tooltips for elements with the * custom attribute "data-tooltip-html". The tooltips are styled using Tailwind CSS classes * to support both light and dark themes and include a dynamically positioned arrow. * * Make sure the FloatingUIDOM global is available. * For example, include in your base template: * */ const { computePosition, offset, flip, shift, arrow } = FloatingUIDOM; document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('[data-tooltip-html]').forEach((el) => { let tooltipContainer = null; let arrowElement = null; let fadeOutTimeout; const showTooltip = () => { if (tooltipContainer) return; // Tooltip already visible // Retrieve the custom HTML content from the data attribute const tooltipContent = el.getAttribute('data-tooltip-html'); // Create a container for the tooltip (with modern styling) tooltipContainer = document.createElement('div'); tooltipContainer.classList.add( 'bg-black', 'text-white', 'shadow-lg', 'rounded-lg', 'p-2', // Transition classes for simple fade in/out 'transition-opacity', 'duration-200', 'opacity-0' ); tooltipContainer.style.position = 'absolute'; tooltipContainer.style.zIndex = '9999'; // Set the HTML content for the tooltip tooltipContainer.innerHTML = '
' + tooltipContent + '
'; // Create the arrow element. The arrow is styled as a small rotated square. arrowElement = document.createElement('div'); arrowElement.classList.add( 'w-3', 'h-3', 'bg-black', 'transform', 'rotate-45' ); arrowElement.style.position = 'absolute'; // Append the arrow into the tooltip container tooltipContainer.appendChild(arrowElement); // Append the tooltip container to the document body document.body.appendChild(tooltipContainer); // Use Floating UI to position the tooltip, including the arrow middleware computePosition(el, tooltipContainer, { middleware: [ offset(8), flip(), shift({ padding: 5 }), arrow({ element: arrowElement }) ] }).then(({ x, y, placement, middlewareData }) => { Object.assign(tooltipContainer.style, { left: `${x}px`, top: `${y}px` }); // Position the arrow using the arrow middleware data const { x: arrowX, y: arrowY } = middlewareData.arrow || {}; // Reset any previous inline values arrowElement.style.left = ''; arrowElement.style.top = ''; arrowElement.style.right = ''; arrowElement.style.bottom = ''; // Adjust the arrow's position according to the placement if (placement.startsWith('top')) { arrowElement.style.bottom = '-4px'; arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%'; } else if (placement.startsWith('bottom')) { arrowElement.style.top = '-4px'; arrowElement.style.left = arrowX !== undefined ? `${arrowX}px` : '50%'; } else if (placement.startsWith('left')) { arrowElement.style.right = '-4px'; arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%'; } else if (placement.startsWith('right')) { arrowElement.style.left = '-4px'; arrowElement.style.top = arrowY !== undefined ? `${arrowY}px` : '50%'; } }); // Trigger a fade-in by moving from opacity-0 to opacity-100 requestAnimationFrame(() => { tooltipContainer.classList.remove('opacity-0'); tooltipContainer.classList.add('opacity-100'); }); }; const hideTooltip = () => { if (tooltipContainer) { tooltipContainer.classList.remove('opacity-100'); tooltipContainer.classList.add('opacity-0'); // Remove the tooltip from the DOM after the transition duration fadeOutTimeout = setTimeout(() => { if (tooltipContainer && tooltipContainer.parentNode) { tooltipContainer.parentNode.removeChild(tooltipContainer); } tooltipContainer = null; arrowElement = null; }, 200); // Matches the duration-200 class (200ms) } }; // Attach event listeners to show/hide the tooltip el.addEventListener('mouseenter', showTooltip); el.addEventListener('mouseleave', hideTooltip); el.addEventListener('focus', showTooltip); el.addEventListener('blur', hideTooltip); }); });