PageSourceSearch

https://www.ziptrak.com.au/wp-content/themes/wp-theme-ziptrak/includes/js/components/tooltips.service.js

js ziptrak.com.au collected 2026-10-08 01:07:27 UTC 8,132 bytes, 293 lines download raw bytes

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.