/** * Mobile-Friendly Tooltip Manager * * Enhances Bootstrap tooltips to work properly on mobile devices by: * - Supporting both click (mobile) and hover (desktop) interactions * - Auto-hiding previous tooltips when a new one is shown * - Closing tooltips when clicking outside * - Reinitializing tooltips for dynamically loaded content */ (function () { 'use strict'; const TOOLTIP_CONFIG = { trigger: 'manual', html: false, sanitize: true, placement: 'top', delay: { show: 200, hide: 300 } }; const MOBILE_CONFIG = { longPressDelay: 500, tapDelay: 300 }; let tooltipInstances = []; function initializeTooltips() { destroyAllTooltips(); const tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]'); if (tooltipElements.length === 0) { return; } tooltipElements.forEach(function (element) { createTooltipInstance(element); }); setupGlobalClickHandler(tooltipElements); } /** * Creates a single tooltip instance with enhanced mobile behavior * @param {HTMLElement} element - The element to attach the tooltip to */ function createTooltipInstance(element) { const tooltip = new bootstrap.Tooltip(element, TOOLTIP_CONFIG); tooltipInstances.push(tooltip); const isLink = element.tagName.toLowerCase() === 'a' && element.href; if (isLink) { setupMobileLinkTooltipBehavior(element, tooltip); } else { setupMobileTooltipBehavior(element, tooltip); } setupDesktopTooltipBehavior(element, tooltip); element.addEventListener('shown.bs.tooltip', function () { hideOtherTooltips(element); }); // Add styling casses if (element.classList.contains('setlist-footnote')) { element.classList.add('mobile-tooltip-symbol'); } else if (element.tagName.toLowerCase() === 'sup') { element.classList.add('mobile-tooltip-number'); } } /** * Sets up mobile-specific tooltip behavior with long press * @param {HTMLElement} element - The tooltip trigger element * @param {bootstrap.Tooltip} tooltip - The tooltip instance */ function setupMobileTooltipBehavior(element, tooltip) { let longPressTimer = null; let isLongPress = false; let touchStartTime = 0; element.addEventListener('contextmenu', function (e) { e.preventDefault(); return false; }); element.addEventListener('touchstart', function (e) { touchStartTime = Date.now(); isLongPress = false; e.preventDefault(); longPressTimer = setTimeout(function () { isLongPress = true; if (!tooltip._isShown()) { tooltip.show(); element.style.backgroundColor = 'rgba(0,0,0,0.1)'; setTimeout(function () { element.style.backgroundColor = ''; }, 200); } }, MOBILE_CONFIG.longPressDelay); }, { passive: false }); element.addEventListener('touchend', function (e) { clearTimeout(longPressTimer); const touchDuration = Date.now() - touchStartTime; if (!isLongPress && touchDuration < MOBILE_CONFIG.tapDelay) { if (tooltip._isShown()) { tooltip.hide(); } else { tooltip.show(); } } isLongPress = false; }); element.addEventListener('touchcancel', function (e) { clearTimeout(longPressTimer); isLongPress = false; }); } /** * Sets up mobile-specific behavior for links with tooltips * Links should navigate normally on tap, show tooltip only on long press * @param {HTMLElement} element - The link element with tooltip * @param {bootstrap.Tooltip} tooltip - The tooltip instance */ function setupMobileLinkTooltipBehavior(element, tooltip) { let longPressTimer = null; let isLongPress = false; let touchStartTime = 0; element.addEventListener('contextmenu', function (e) { e.preventDefault(); return false; }); element.addEventListener('touchstart', function (e) { touchStartTime = Date.now(); isLongPress = false; longPressTimer = setTimeout(function () { isLongPress = true; if (!tooltip._isShown()) { tooltip.show(); element.style.backgroundColor = 'rgba(0,0,0,0.1)'; setTimeout(function () { element.style.backgroundColor = ''; }, 200); } }, MOBILE_CONFIG.longPressDelay); }); element.addEventListener('touchend', function (e) { clearTimeout(longPressTimer); if (isLongPress) { e.preventDefault(); } isLongPress = false; }); element.addEventListener('touchcancel', function (e) { clearTimeout(longPressTimer); isLongPress = false; }); element.addEventListener('click', function (e) { if ('ontouchstart' in window) { // On touch devices, if tooltip is showing, hide it and prevent navigation if (tooltip._isShown()) { e.preventDefault(); tooltip.hide(); return false; } } }); } /** * Sets up desktop hover behavior for tooltips with debouncing * @param {HTMLElement} element - The tooltip trigger element * @param {bootstrap.Tooltip} tooltip - The tooltip instance */ function setupDesktopTooltipBehavior(element, tooltip) { let showTimer = null; let hideTimer = null; let isHovered = false; function clearTimers() { if (showTimer) clearTimeout(showTimer); if (hideTimer) clearTimeout(hideTimer); showTimer = null; hideTimer = null; } element.addEventListener('mouseenter', function (e) { if (!('ontouchstart' in window)) { isHovered = true; clearTimers(); showTimer = setTimeout(function () { if (isHovered && !tooltip._isShown()) { tooltip.show(); } }, 200); } }); element.addEventListener('mouseleave', function (e) { if (!('ontouchstart' in window)) { isHovered = false; clearTimers(); hideTimer = setTimeout(function () { if (!isHovered && tooltip._isShown()) { tooltip.hide(); } }, 300); } }); element.addEventListener('shown.bs.tooltip', function () { const tooltipElement = document.querySelector('.tooltip'); if (tooltipElement && !('ontouchstart' in window)) { tooltipElement.addEventListener('mouseenter', function () { clearTimers(); isHovered = true; }); tooltipElement.addEventListener('mouseleave', function () { isHovered = false; hideTimer = setTimeout(function () { if (!isHovered && tooltip._isShown()) { tooltip.hide(); } }, 300); }); } }); element.addEventListener('click', function (e) { if (!('ontouchstart' in window)) { clearTimers(); if (tooltip._isShown()) { tooltip.hide(); } else { tooltip.show(); } } }); } /** * Hides all tooltips except the one attached to the specified element * @param {HTMLElement} excludeElement - Element whose tooltip should stay visible */ function hideOtherTooltips(excludeElement) { tooltipInstances.forEach(function (tooltipInstance) { const tooltipElement = tooltipInstance._element; if (tooltipElement && tooltipElement !== excludeElement) { tooltipInstance.hide(); } }); } /** * Sets up global click handler to close tooltips when clicking outside * @param {NodeList} tooltipElements - All tooltip trigger elements */ function setupGlobalClickHandler(tooltipElements) { document.removeEventListener('click', globalClickHandler); document.addEventListener('click', globalClickHandler); } /** * Global click handler function (defined separately for easier removal) * @param {Event} event - The click event */ function globalClickHandler(event) { const clickedElement = event.target; const isTooltipTrigger = clickedElement.closest('[data-bs-toggle="tooltip"]'); const isTooltipPopup = clickedElement.closest('.tooltip'); if (!isTooltipTrigger && !isTooltipPopup) { hideAllTooltips(); } } /** * Hides all currently visible tooltips */ function hideAllTooltips() { tooltipInstances.forEach(function (tooltipInstance) { if (tooltipInstance._element) { tooltipInstance.hide(); } }); } /** * Destroys all tooltip instances and cleans up event listeners */ function destroyAllTooltips() { tooltipInstances.forEach(function (tooltipInstance) { if (tooltipInstance._element) { tooltipInstance.dispose(); } }); tooltipInstances = []; document.removeEventListener('click', globalClickHandler); } /** * Sets up mutation observer to reinitialize tooltips when new content is added */ function setupDynamicContentWatcher() { const observer = new MutationObserver(function (mutations) { let shouldReinit = false; mutations.forEach(function (mutation) { if (mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(function (node) { if (node.nodeType === 1) { // Element node if (node.hasAttribute && node.hasAttribute('data-bs-toggle') || node.querySelector && node.querySelector('[data-bs-toggle="tooltip"]')) { shouldReinit = true; } } }); } }); if (shouldReinit) { setTimeout(initializeTooltips, 100); } }); observer.observe(document.body, { childList: true, subtree: true }); } window.MobileTooltips = { init: initializeTooltips, destroy: destroyAllTooltips, hideAll: hideAllTooltips, getInstances: function () { return tooltipInstances; } }; $(document).ready(function () { initializeTooltips(); setupDynamicContentWatcher(); }); })();