1/** 2 * TooltipService class to manage popups (tooltips) 3 * using the Floating UI library. 4 */ 5class TooltipService { 6 /** 7 * Creates an instance of the service. 8 */ 9 constructor() { 10 // Check if Floating UI DOM library is loaded 11 if (!window.FloatingUIDOM) { 12 console.warn('TooltipService: Floating UI DOM library is not loaded.'); 13 this.isLoaded = false; 14 return; 15 } 16 17 this.isLoaded = true; 18 this.floatingUI = window.FloatingUIDOM; 19 this.cleanupAutoUpdate = null; 20 21 this.config = { 22 selectors: { 23 trigger: '[data-tooltip-trigger]', 24 arrow: '[data-popup-arrow]', 25 allActivePopups: '.ziptrak__popup.is-active', 26 popupContainer: '.ziptrak__popup', 27 }, 28 classes: { 29 active: 'is-active', 30 }, 31 attributes: { 32 triggerId: 'data-tooltip-trigger', 33 } 34 }; 35 36 // Explicit context binding for native event handlers 37 this.handleMouseOver = this.handleMouseOver.bind(this); 38 this.handleMouseOut = this.handleMouseOut.bind(this); 39 this.handleClick = this.handleClick.bind(this); 40 this.handleDismiss = this.handleDismiss.bind(this); 41 } 42 43 /** 44 * Initializes the service and attaches global event listeners. 45 * Supports dynamically loaded AJAX elements via event delegation. 46 * @returns {void} 47 */ 48 init() { 49 if (!this.isLoaded) return; 50 51 // Desktop: Hover behaviors 52 document.addEventListener('mouseover', this.handleMouseOver); 53 document.addEventListener('mouseout', this.handleMouseOut); 54 55 // Mobile & Global: Click behavior (Light Dismiss & Toggle) 56 document.addEventListener('click', this.handleClick); 57 document.addEventListener('touchmove', this.handleDismiss, {passive: true, capture: true}); 58 } 59 60 /** 61 * Removes all event listeners and cleans up active position tracking (Destructor). 62 * @returns {void} 63 */ 64 destroy() { 65 if (!this.isLoaded) return; 66 67 document.removeEventListener('mouseover', this.handleMouseOver); 68 document.removeEventListener('mouseout', this.handleMouseOut); 69 document.removeEventListener('click', this.handleClick); 70 document.removeEventListener('touchmove', this.handleDismiss); 71 72 if (this.cleanupAutoUpdate) { 73 this.cleanupAutoUpdate(); 74 this.cleanupAutoUpdate = null; 75 } 76 } 77 78 /** 79 * Calculates and updates the coordinates of the tooltip popup. 80 * @param {HTMLElement} trigger - The element that triggered the tooltip 81 * @param {HTMLElement} popup - The tooltip popup element itself 82 * @param {HTMLElement|null} arrowElement - The arrow element inside the popup 83 * @returns {void} 84 */ 85 updateTooltip(trigger, popup, arrowElement) { 86 const {computePosition, flip, shift, offset, arrow} = this.floatingUI; 87 88 const middleware = [ 89 offset(8), 90 flip(), 91 shift({padding: 20}), 92 arrowElement ? arrow({element: arrowElement}) : null 93 ]; 94 95 const options = {placement: 'top', strategy: 'absolute', middleware}; 96 97 computePosition(trigger, popup, options).then(({x, y, placement, middlewareData}) => { 98 // Apply calculated coordinates to the popup 99 Object.assign(popup.style, {left: `${x}px`, top: `${y}px`}); 100 101 // Position the arrow if it exists 102 if (arrowElement && middlewareData.arrow) { 103 const {x: arrowX, y: arrowY} = middlewareData.arrow; 104 105 const side = placement.split('-')[0]; 106 const staticSide = {top: 'bottom', bottom: 'top', left: 'right', right: 'left'}[side]; 107 108 Object.assign(arrowElement.style, { 109 left: arrowX != null ? `${arrowX}px` : '', 110 top: arrowY != null ? `${arrowY}px` : '', 111 right: '', 112 bottom: '', 113 [staticSide]: '-4px', // Shift arrow halfway out of the popup boundary 114 }); 115 } 116 }); 117 } 118 119 /** 120 * Displays the tooltip and attaches dynamic position tracking. 121 * @param {HTMLElement} trigger - The element that triggered the tooltip 122 * @param {HTMLElement} popup - The tooltip popup element itself 123 * @param {HTMLElement|null} arrowElement - The arrow element inside the popup 124 * @returns {void} 125 */ 126 showTooltip(trigger, popup, arrowElement) { 127 // If another tooltip tracking is active, clean it up first 128 if (this.cleanupAutoUpdate) { 129 this.cleanupAutoUpdate(); 130 } 131 132 // Move popup to body to escape parent transform context (e.g., Swiper slides) 133 if (popup.parentNode !== document.body) {
134 document.body.appendChild(popup); 135 } 136 137 popup.classList.add(this.config.classes.active); 138 139 // Dynamically update position on scroll, resize, or element movement 140 this.cleanupAutoUpdate = this.floatingUI.autoUpdate( 141 trigger, 142 popup, 143 () => this.updateTooltip(trigger, popup, arrowElement) 144 ); 145 } 146 147 /** 148 * Completely hides the active tooltip and removes its position listeners. 149 * @param {HTMLElement|null} popup - The tooltip popup element to hide 150 * @returns {void} 151 */ 152 hideTooltip(popup) { 153 if (popup) { 154 popup.classList.remove(this.config.classes.active); 155 } 156 157 if (this.cleanupAutoUpdate) { 158 this.cleanupAutoUpdate(); 159 this.cleanupAutoUpdate = null; 160 } 161 } 162 163 /** 164 * Retrieves the popup element and its internal arrow based on the trigger element. 165 * @param {HTMLElement} trigger - The current tooltip trigger element 166 * @returns {{popup: HTMLElement|null, arrowElement: HTMLElement|null}} Found tooltip elements 167 */ 168 getTooltipElements(trigger) { 169 const popupId = trigger.getAttribute(this.config.attributes.triggerId); 170 171 if (!popupId) { 172 return {popup: null, arrowElement: null}; 173 } 174 175 const popup = document.getElementById(popupId); 176 const arrowElement = popup ? popup.querySelector(this.config.selectors.arrow) : null; 177 178 return {popup, arrowElement}; 179 } 180 181 /** 182 * Closes all currently active tooltip popups. 183 * @returns {void} 184 */ 185 closeAllActivePopups() { 186 document.querySelectorAll(this.config.selectors.allActivePopups).forEac
186h(popup => { 187 this.hideTooltip(popup); 188 }); 189 } 190 191 // --- Native Event Handlers --- 192 193 /** 194 * Handler to dismiss all active tooltips (on scroll or slider touch move). 195 * @returns {void} 196 */ 197 handleDismiss() { 198 this.closeAllActivePopups(); 199 } 200 201 /** 202 * Handler for Desktop Hover (In). 203 * @param {MouseEvent} e - The mouse event object 204 * @returns {void} 205 */ 206 handleMouseOver(e) { 207 // If it's a touch screen device, completely ignore hover events 208 if (window.matchMedia('(pointer: coarse)').matches) { 209 return; 210 } 211 212 const trigger = e.target.closest(this.config.selectors.trigger); 213 214 if (!trigger) { 215 return; 216 } 217 218 const {popup, arrowElement} = this.getTooltipElements(trigger); 219 220 if (popup) { 221 this.showTooltip(trigger, popup, arrowElement); 222 } 223 } 224 225 /** 226 * Handler for Desktop Hover (Out). 227 * @param {MouseEvent} e - The mouse event object 228 * @returns {void} 229 */ 230 handleMouseOut(e) { 231 // If it's a touch screen device, completely ignore hover events 232 if (window.matchMedia('(pointer: coarse)').matches) { 233 return; 234 } 235 236 const trigger = e.target.closest(this.config.selectors.trigger); 237 238 if (!trigger) { 239 return; 240 } 241 242 const {popup} = this.getTooltipElements(trigger); 243 this.hideTooltip(popup); 244 } 245 246 /** 247 * Native Click Handler (Handles Mobile Taps + Light Dismiss + Prevents Link Navigation). 248 * @param {MouseEvent} e - The click event object 249 * @returns {void} 250 */ 251 handleClick(e) { 252 const trigger = e.target.closest(this.config.selectors.trigger); 253 254 // CASE 1: User clicked on a valid tooltip trigger 255 if (trigger) { 256 if (!window.matchMedia('(pointer: coarse)').matches) { 257 return; 258 } 259 260 e.preventDefault(); 261 e.stopPropagation(); // Stop event from bubbling up to the <a> link 262 263 const {popup, arrowElement} = this.getTooltipElements(trigger); 264 265 if (!popup) { 266 return; 267 } 268 269 if (popup.classList.contains(this.config.classes.active)) { 270 this.hideTooltip(popup); 271 } else { 272 this.closeAllActivePopups(); 273 this.showTooltip(trigger, popup, arrowElement); 274 } 275 return; 276 } 277 278 // CASE 2: User clicked inside the opened popup content itself 279 if (e.target.closest(this.config.selectors.popupContainer)) { 280 e.stopPropagation(); 281 return; 282 } 283 284 // CASE 3: User clicked outside (Light Dismiss) 285 this.closeAllActivePopups(); 286 } 287} 288 289// Automatically instantiate and start the service once DOM is ready 290document.addEventListener('DOMContentLoaded', () => { 291 const tooltips = new TooltipService(); 292 tooltips.init(); 293});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.