import { computePosition, flip, shift, offset, } from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.12/+esm'; import { devlog } from '/devlog.js'; class SimpleTooltipManager { constructor() { devlog('๐Ÿ”ง Tooltip Manager: Constructor called'); this.tooltips = new Map(); this.dismissedPersistentTooltips = new Set(); this.init(); } getElementId(element) { // Create a unique ID for the element based on its characteristics if (element.matches('g.spvb-open-templates-button')) { return 'templates-button'; } if (element.matches('button#btn-edit')) { return 'edit-button'; } if (element.matches('button#btn-walkthrough')) { return 'walkthrough-button'; } // Safely handle className for fallback ID let className = ''; try { // Handle both string className and SVG className objects className = typeof element.className === 'string' ? element.className : element.className.baseVal || ''; // Sanitize className by removing special characters and normalizing className = className.replace(/[^a-zA-Z0-9\s-_]/g, '').replace(/\s+/g, '-'); } catch (e) { // If className access fails, use empty string className = ''; } // Create fallback ID with safe tag name and className const tagName = element.tagName ? element.tagName.toLowerCase() : 'element'; const classNamePart = className ? `-${className}` : ''; return `${tagName}${classNamePart}`; } init() { devlog('โš™๏ธ Tooltip Manager: Initializing...'); // Add CSS styles this.addStyles(); // Start the interval to check for new elements devlog('โฐ Tooltip Manager: Setting up interval'); setInterval(() => { // devlog('๐Ÿ”„ Tooltip Manager: Interval running - checking for elements'); this.addTooltipsToElements(); }, 100); // Initial run devlog('๐Ÿƒ Tooltip Manager: Running initial element check'); this.addTooltipsToElements(); } addStyles() { if (document.getElementById('simple-tooltip-styles')) { devlog('๐ŸŽจ Tooltip Manager: Styles already exist, skipping'); return; } const style = document.createElement('style'); style.id = 'simple-tooltip-styles'; style.textContent = ` .simple-tooltip { user-select: none; pointer-events: none; position: absolute; background-color: #1f2937; color: white; padding: 0.5rem 0.75rem; border-radius: 0.25rem; font-size: 0.875rem; line-height: 1.4; max-width: 296px; z-index: 1002; opacity: 0; visibility: hidden; /* transition: opacity 0.2s, visibility 0.2s, left 0.15s ease-out, top 0.15s ease-out; */ transition: opacity 0s, visibility 0s, left 0s ease-out, top 0s ease-out; pointer-events: auto; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .simple-tooltip.persistent { pointer-events: none; } .simple-tooltip.show { opacity: 1; visibility: visible; } /* Speech bubble arrow for white variant */ .simple-tooltip.white::before { content: ''; position: absolute; top: -8px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 8px solid #ffffff; z-index: 1003; } .simple-tooltip.white::after { content: ''; position: absolute; top: -9px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-bottom: 9px solid #e5e7eb; z-index: 1002; } /* Variant styles */ .simple-tooltip.green { background-color: #047857; color: white; } .simple-tooltip.white { background-color: #ffffff; color: #1f2937; border: 1px solid #e5e7eb; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); padding: 12px 16px; border-radius: 12px; font-size: 14px; font-weight: 500; max-width: 200px; text-align: center; } .simple-tooltip.purple { background-color: #7e22ce; color: white; } .simple-tooltip.red { background-color: #dc2626; color: white; } /* Pulsating dot animation - similar to edit button */ .tooltip-pulsating-dot { animation: pulseEffect 2s infinite ease-out; will-change: transform, opacity; backface-visibility: hidden; } .tooltip-center-dot { /* Static center dot - no animation */ } @keyframes pulseEffect { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(6); opacity: 0; } } `; document.head.appendChild(style); } addPulsatingDots(element) { // Get element position in viewport const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; // Create pulsating dot (animated) as HTML element const pulsatingDot = document.createElement('div'); pulsatingDot.className = 'tooltip-pulsating-dot'; pulsatingDot.style.position = 'fixed'; pulsatingDot.style.left = `${centerX - 8}px`; pulsatingDot.style.top = `${centerY - 8}px`; pulsatingDot.style.width = '16px'; pulsatingDot.style.height = '16px'; pulsatingDot.style.backgroundColor = '#3161d1'; pulsatingDot.style.borderRadius = '50%'; pulsatingDot.style.pointerEvents = 'none'; pulsatingDot.style.zIndex = '888'; const oldPulsatingDot = document.querySelector('.tooltip-pulsating-dot'); if (oldPulsatingDot) { oldPulsatingDot.remove(); } // Append pulsating dot to body if (!document.body.querySelector('.tooltip-pulsating-dot')) { document.body.appendChild(pulsatingDot); } // Store references for cleanup element._pulsatingDot = pulsatingDot; devlog('โœจ Tooltip Manager: Created pulsating HTML dots for element'); } addTooltipsToElements() { const selectors = [ // '.sp-inserter-connection-thumbs', '#add_new_btn', '.search-container', '.left-sidebar > div', '.page-actions > button', '.site-actions > button', '.topnav-icons > svg', '.topnav-icons .profile-pic', '.topnav .logo', '.site-nav > a', '.page-status > button', // '.demo-btn', // '.trial-btn', 'g.spvb-open-templates-button', 'button#btn-walkthrough', '.sp-inserter-image-field .sp-inserter-input-group-btn', '.sp-inserter-form-text .sp-inserter-input-group-btn', '.spvb-mediainput-button', '.sp-inserter-connection-tiles', '.shortpoint-inserter-visibility-form-sp-user-in-group', ]; if (!window.location.href.includes('template')) { selectors.push('button#btn-edit'); } // devlog( // '๐Ÿ” Tooltip Manager: Searching for elements with multiple selectors' // ); const elements = document.querySelectorAll(selectors.join(', ')); // devlog(`๐Ÿ“Š Tooltip Manager: Found ${elements.length} elements`); if (elements.length === 0) { devlog( 'โš ๏ธ Tooltip Manager: No elements found with any of the specified selectors' ); return; } // Remove 'disabled' class and 'data-shortpoint-bind' attribute from relevant elements const disabledThumbs = document.querySelectorAll( '.sp-inserter-connection-thumbs.disabled' ); disabledThumbs.forEach(el => { el.classList.remove('disabled'); el.removeAttribute('data-shortpoint-bind'); devlog( '๐Ÿงน Tooltip Manager: Removed disabled class and data-shortpoint-bind from', el ); }); let processedCount = 0; let skippedCount = 0; elements.forEach((element, index) => { // Check if element is visible (has dimensions) const rect = element.getBoundingClientRect(); const isVisible = rect.width > 0 && rect.height > 0; // Check if this is a dismissed persistent tooltip const elementId = this.getElementId(element); const wasDismissed = this.dismissedPersistentTooltips.has(elementId); const wouldBePersistent = element.matches('g.spvb-open-templates-button'); // Skip if this persistent tooltip was already dismissed if (wouldBePersistent && wasDismissed) { skippedCount++; devlog(`โญ๏ธ Tooltip Manager: Skipping dismissed persistent tooltip ${elementId}`); return; } // Skip if already processed AND visible (only reprocess invisible elements) const alreadyProcessed = element.getAttribute('tooltip-added') === 'true'; const persistentDisabled = element._tooltipPersistent === false; const tooltipDisabled = element._tooltipDisabled === true; // Skip if tooltip is permanently disabled if (tooltipDisabled) { skippedCount++; return; } if (alreadyProcessed && isVisible) { skippedCount++; return; } // Skip if element is not visible (but don't mark as processed) if (!isVisible) { // Remove the attribute if it exists so it can be reprocessed when visible if (alreadyProcessed) { element.removeAttribute('tooltip-added'); devlog( `๐Ÿ”„ Tooltip Manager: Removed tooltip-added attribute from invisible element ${index + 1}` ); } skippedCount++; return; } devlog(`โœ… Tooltip Manager: Adding tooltip to element ${index + 1}`); // Add the data attribute element.setAttribute('tooltip-added', 'true'); devlog( `๐Ÿท๏ธ Tooltip Manager: Added tooltip-added attribute to element ${index + 1}` ); // Create tooltip content (customize this as needed) const tooltipContent = this.getTooltipContent(element); devlog( `๐Ÿ“ Tooltip Manager: Tooltip content for element ${index + 1}: "${tooltipContent}"` ); // elementId already calculated above // Add event listeners (make g elements persistent and highlighted, unless disabled or dismissed) const isPersistent = element.matches('g.spvb-open-templates-button') && persistentDisabled !== true && !wasDismissed; const isHighlighted = element.matches('g.spvb-open-templates-button') && persistentDisabled !== true && !wasDismissed; this.attachTooltipEvents( element, tooltipContent, 'default', isPersistent, elementId ); devlog( `๐ŸŽง Tooltip Manager: Attached event listeners to element ${index + 1} (persistent: ${isPersistent}, highlighted: ${isHighlighted})` ); // Add pulsating effect to the element if highlighted (with delay) if (isHighlighted) { setTimeout(() => { // Double-check element is still visible and not dismissed before adding animation const rect = element.getBoundingClientRect(); const isStillVisible = rect.width > 0 && rect.height > 0; if (isStillVisible && !this.dismissedPersistentTooltips.has(elementId)) { this.addPulsatingDots(element); devlog( `โœจ Tooltip Manager: Added pulsating dots to ${elementId} after delay` ); } else { devlog( `โญ๏ธ Tooltip Manager: Skipped adding pulsating animation - element no longer visible or was dismissed` ); } }, 1800); // 1.8 second delay for animation } // Show persistent tooltips after a delay to ensure element is fully visible if (isPersistent && element._showTooltip) { setTimeout(() => { // Double-check element is still visible before showing tooltip const rect = element.getBoundingClientRect(); const isStillVisible = rect.width > 0 && rect.height > 0; if (isStillVisible && !this.dismissedPersistentTooltips.has(elementId)) { const fakeEvent = { clientX: rect.left + rect.width / 2, clientY: rect.bottom, stopPropagation: () => {}, preventDefault: () => {}, }; // Directly call the showTooltip function element._showTooltip(fakeEvent); devlog( `โœจ Tooltip Manager: Showed persistent tooltip for ${elementId} after delay` ); } else { devlog( `โญ๏ธ Tooltip Manager: Skipped showing persistent tooltip - element no longer visible or was dismissed` ); } }, 1500); // 1.5 second delay to ensure element is stable and visible } processedCount++; }); // devlog( // `๐Ÿ“ˆ Tooltip Manager: Processing complete - ${processedCount} new, ${skippedCount} skipped` // ); // Add a manual test function to the first element (for debugging) if (processedCount > 0 && elements.length > 0) { const firstElement = elements[0]; devlog( '๐Ÿงช Tooltip Manager: Adding manual test - you can call window.testTooltip() to trigger a tooltip on the first element' ); window.testTooltip = () => { devlog('๐Ÿงช Manual tooltip test triggered'); firstElement.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); }; } } removeExistingNonPersistentTooltips(currentTooltip) { const existingTooltips = document.querySelectorAll( '.simple-tooltip:not(.persistent)' ); existingTooltips.forEach(existingTooltip => { if (existingTooltip !== currentTooltip) { existingTooltip.classList.remove('show'); setTimeout(() => { if (existingTooltip && existingTooltip.parentNode) { existingTooltip.parentNode.removeChild(existingTooltip); devlog('๐Ÿ—‘๏ธ Tooltip Manager: Removed existing non-persistent tooltip'); } }, 0); } }); } getTooltipContent(element) { // Custom tooltip for button#btn-edit if ( element.matches('button#btn-edit') && !window.location.href.includes('templates') ) { return 'Edit this page with ShortPoint!'; } // Custom tooltip for button#btn-walkthrough if (element.matches('button#btn-walkthrough')) { return 'Get a walkthrough with a ShortPoint Expert!'; } if (element.matches('.spvb-open-templates-button')) { return 'Click here to start with a Template'; } // if element has a title then remove it if (element.title) { element.removeAttribute('title'); } // Customize this method to return appropriate tooltip content // You can inspect the element and return different content based on its properties return ( element.getAttribute('data-tooltip') || // element.title || `Disabled because this is a sandbox environment Start a trial to access all of ShortPoint's features` ); } attachTooltipEvents( element, content, variant = 'default', persistent = false, elementId = null ) { let tooltip = null; let showTimeout = null; let hideTimeout = null; let visibilityInterval = null; // Determine variant and offset for this element const isEditBtn = element.matches('button#btn-edit'); const isDemoBtn = element.matches('.demo-btn'); const isTrialBtn = element.matches('.trial-btn'); const isGElement = element.matches('g.spvb-open-templates-button'); // Use white variant specifically for g elements if (isGElement) { variant = 'white'; } // Minimal offset for g elements, normal offset for others const tooltipOffset = isGElement ? 10 : isEditBtn || isDemoBtn || isTrialBtn ? 30 : 50; const showTooltip = e => { e.stopPropagation(); devlog('๐Ÿ–ฑ๏ธ Tooltip Manager: Mouse enter event triggered', element); e.preventDefault(); // Clear any existing timeouts if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; devlog('โฐ Tooltip Manager: Cleared hide timeout'); } // Remove any existing non-persistent tooltips before showing new one this.removeExistingNonPersistentTooltips(tooltip); // Create tooltip if it doesn't exist or if it was removed from DOM if (!tooltip || !tooltip.parentNode) { devlog('๐Ÿ†• Tooltip Manager: Creating new tooltip'); tooltip = this.createTooltip(content, variant, persistent); // Establish owner relationship for cleanup purposes tooltip._ownerElement = element; document.body.appendChild(tooltip); // Add hover event listeners for non-persistent tooltips if (!persistent) { tooltip.addEventListener('mouseenter', () => { if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; devlog('โฐ Tooltip Manager: Cancelled hide timeout - tooltip hovered'); } }); tooltip.addEventListener('mouseleave', () => { // Hide tooltip when leaving tooltip hideTimeout = setTimeout(() => { // Check both tooltip and element hover state const elementHovered = element.matches(':hover'); const tooltipHovered = tooltip && tooltip.matches(':hover'); if (tooltip && !elementHovered && !tooltipHovered) { tooltip.classList.remove('show'); setTimeout(() => { if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); tooltip = null; devlog('๐Ÿ—‘๏ธ Tooltip Manager: Tooltip removed after leaving tooltip'); } }, 200); } else { devlog( '๐Ÿ–ฑ๏ธ Tooltip Manager: Element still hovered after leaving tooltip, keeping visible' ); } }, 100); }); } devlog('โž• Tooltip Manager: Tooltip added to DOM'); } // Position and show tooltip at cursor location showTimeout = setTimeout(() => { this.positionTooltipAtCursor(tooltip, e.clientX, e.clientY, tooltipOffset) .then(() => { tooltip.classList.add('show'); // For persistent tooltips, prevent any future position updates and start visibility checking if (persistent) { tooltip._isFixed = true; startVisibilityCheck(); } }) .catch(error => { devlog('โŒ Tooltip Manager: Error positioning tooltip:', error); }); }, 0); // You can add delay here if needed }; const updateTooltipPosition = e => { e.preventDefault(); // Don't update position for persistent tooltips if (persistent) { return; } if (tooltip && tooltip.classList.contains('show')) { this.positionTooltipAtCursor(tooltip, e.clientX, e.clientY, tooltipOffset); } }; const hideTooltip = e => { // Don't hide persistent tooltips on mouse leave if (persistent) { return; } e.stopPropagation(); e.preventDefault(); devlog('๐Ÿ–ฑ๏ธ Tooltip Manager: Mouse leave event triggered', element); if (showTimeout) { clearTimeout(showTimeout); showTimeout = null; devlog('โฐ Tooltip Manager: Cleared show timeout'); } if (tooltip) { devlog('๐Ÿซฅ Tooltip Manager: Hiding tooltip'); hideTimeout = setTimeout(() => { // Double-check that we're not hovering over the tooltip itself or the element const elementHovered = element.matches(':hover'); const tooltipHovered = tooltip && tooltip.matches(':hover'); if (tooltip && !elementHovered && !tooltipHovered) { tooltip.classList.remove('show'); setTimeout(() => { if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); tooltip = null; devlog('๐Ÿ—‘๏ธ Tooltip Manager: Tooltip removed from DOM'); } }, 200); // Wait for transition to complete } else { devlog( '๐Ÿ–ฑ๏ธ Tooltip Manager: Element or tooltip still being hovered, keeping visible' ); } }, 100); // Small delay before hiding } }; const handleTooltipClick = e => { devlog('๐Ÿ–ฑ๏ธ Tooltip Manager: Click event triggered', element); if (showTimeout) { clearTimeout(showTimeout); showTimeout = null; devlog('โฐ Tooltip Manager: Cleared show timeout'); } // For non-persistent tooltips, hide immediately if (!persistent) { if (tooltip) { devlog('๐Ÿซฅ Tooltip Manager: Hiding non-persistent tooltip'); tooltip.classList.remove('show'); setTimeout(() => { if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); tooltip = null; devlog('๐Ÿ—‘๏ธ Tooltip Manager: Non-persistent tooltip removed from DOM'); } }, 0); } return; } // For persistent tooltips, disable them permanently after click devlog('๐Ÿ”„ Tooltip Manager: Disabling persistent tooltip permanently'); // Mark this persistent tooltip as dismissed in memory if (elementId && window.tooltipManager) { window.tooltipManager.dismissedPersistentTooltips.add(elementId); devlog(`๐Ÿ’พ Tooltip Manager: Marked ${elementId} as dismissed in memory`); } // Hide the tooltip first if (tooltip) { tooltip.classList.remove('show'); setTimeout(() => { if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); tooltip = null; devlog('๐Ÿ—‘๏ธ Tooltip Manager: Persistent tooltip removed from DOM'); } }, 200); } // Remove pulsating animation if (element._pulsatingDot && element._pulsatingDot.parentNode) { element._pulsatingDot.parentNode.removeChild(element._pulsatingDot); element._pulsatingDot = null; devlog('๐Ÿ—‘๏ธ Tooltip Manager: Pulsating animation removed from DOM'); } // Stop visibility checking stopVisibilityCheck(); // Mark element as permanently disabled for tooltips element._tooltipDisabled = true; element._tooltipPersistent = false; persistent = false; // Update local variable for this handler // Remove ALL tooltip event listeners to disable tooltip functionality element.removeEventListener('mouseenter', showTooltip, { capture: true }); element.removeEventListener('mousemove', updateTooltipPosition); element.removeEventListener('mouseleave', hideTooltip, { capture: true }); element.removeEventListener('focus', showTooltip); element.removeEventListener('blur', hideTooltip); element.removeEventListener('click', handleTooltipClick, { capture: true }); devlog( 'โœ… Tooltip Manager: Persistent tooltip permanently disabled - will not show again' ); }; const startVisibilityCheck = () => { if (visibilityInterval) { clearInterval(visibilityInterval); } visibilityInterval = setInterval(() => { if (tooltip && tooltip.classList.contains('show')) { const rect = element.getBoundingClientRect(); const isVisible = rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; if (!isVisible) { devlog( '๐Ÿ‘ป Tooltip Manager: Element became invisible, hiding persistent tooltip' ); tooltip.classList.remove('show'); setTimeout(() => { if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); tooltip = null; devlog('๐Ÿ—‘๏ธ Tooltip Manager: Hidden persistent tooltip removed from DOM'); } }, 200); // Remove pulsating animation when tooltip is hidden if (element._pulsatingDot && element._pulsatingDot.parentNode) { element._pulsatingDot.parentNode.removeChild(element._pulsatingDot); element._pulsatingDot = null; devlog( '๐Ÿ—‘๏ธ Tooltip Manager: Pulsating animation removed when tooltip hidden' ); } clearInterval(visibilityInterval); visibilityInterval = null; } else { // Update tooltip position to follow element const centerX = rect.left + rect.width / 2; const bottomY = rect.bottom; tooltip._isFixed = false; // Temporarily allow repositioning this.positionTooltipAtCursor(tooltip, centerX, bottomY, tooltipOffset).then( () => { tooltip._isFixed = true; // Re-fix the position } ); // Update pulsating dot position to follow element if (element._pulsatingDot) { const dotCenterY = rect.top + rect.height / 2; element._pulsatingDot.style.left = `${centerX - 8}px`; element._pulsatingDot.style.top = `${dotCenterY - 8}px`; } } } }, 200); }; const stopVisibilityCheck = () => { if (visibilityInterval) { clearInterval(visibilityInterval); visibilityInterval = null; } }; // Store showTooltip function on element for direct access element._showTooltip = showTooltip; // Add event listeners devlog( `๐ŸŽง Tooltip Manager: Adding mouse listeners to element ${element.tagName}.${element.className} (persistent: ${persistent})` ); element.addEventListener('mouseenter', showTooltip, { capture: true }); // Only add mousemove for non-persistent tooltips if (!persistent) { element.addEventListener('mousemove', updateTooltipPosition); } element.addEventListener('mouseleave', hideTooltip, { capture: true }); element.addEventListener('focus', showTooltip); element.addEventListener('blur', hideTooltip); // Add click listener (unified for both persistent and non-persistent) element.addEventListener('click', handleTooltipClick, { capture: true }); // Log element details for debugging const rect = element.getBoundingClientRect(); const computedStyle = window.getComputedStyle(element); devlog(`๐Ÿ“ Tooltip Manager: Element CSS properties:`, { display: computedStyle.display, visibility: computedStyle.visibility, pointerEvents: computedStyle.pointerEvents, zIndex: computedStyle.zIndex, position: computedStyle.position, }); // Store cleanup function this.tooltips.set(element, () => { element.removeEventListener('mouseenter', showTooltip, { capture: true }); if (!persistent) { element.removeEventListener('mousemove', updateTooltipPosition); } element.removeEventListener('mouseleave', hideTooltip, { capture: true }); element.removeEventListener('focus', showTooltip); element.removeEventListener('blur', hideTooltip); element.removeEventListener('click', handleTooltipClick, { capture: true }); if (persistent) { stopVisibilityCheck(); } if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip); } // Clean up pulsating dot if (element._pulsatingDot && element._pulsatingDot.parentNode) { element._pulsatingDot.parentNode.removeChild(element._pulsatingDot); } }); } createTooltip(content, variant = 'default', isPersistent = false) { const tooltip = document.createElement('div'); const persistentClass = isPersistent ? 'persistent' : ''; tooltip.className = `simple-tooltip ${variant !== 'default' ? variant : ''} ${persistentClass}`.trim(); tooltip.innerHTML = content; return tooltip; } positionTooltipAtCursor(tooltip, mouseX, mouseY, offsetValue = 50) { // Don't reposition if tooltip is marked as fixed if (tooltip._isFixed) { return Promise.resolve(); } // Create a virtual element representing the cursor position const virtualElement = { getBoundingClientRect() { return { width: 0, height: 0, x: mouseX, y: mouseY, top: mouseY, left: mouseX, right: mouseX, bottom: mouseY, }; }, }; // Use Floating UI to compute the position relative to cursor return computePosition(virtualElement, tooltip, { placement: 'bottom', middleware: [ offset(offsetValue), // Custom offset from the cursor flip(), // Flip if no space shift({ padding: 8 }), // Keep tooltip within viewport with 8px padding ], }).then(({ x, y }) => { // Apply the computed position Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); // No arrow logic needed }); } positionTooltip(tooltip, element) { // Use Floating UI to compute the position return computePosition(element, tooltip, { placement: 'bottom', middleware: [ offset(8), // 8px offset from the element flip(), // Flip to top if no space below shift({ padding: 8 }), // Keep tooltip within viewport with 8px padding ], }).then(({ x, y }) => { // Apply the computed position Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); // No arrow logic needed }); } // Method to clean up all tooltips (useful for cleanup) destroy() { this.tooltips.forEach(cleanup => cleanup()); this.tooltips.clear(); // Remove styles const styles = document.getElementById('simple-tooltip-styles'); if (styles) styles.remove(); } } // Initialize the tooltip manager when DOM is ready document.addEventListener('DOMContentLoaded', () => { devlog('๐Ÿš€ Tooltip Manager: DOM loaded, initializing...'); try { const tooltipManager = new SimpleTooltipManager(); window.tooltipManager = tooltipManager; devlog('โœ… Tooltip Manager: Successfully initialized'); } catch (error) { devlog('โŒ Tooltip Manager: Failed to initialize:', error); } });