1/** 2 * TrendingTooltip - Bootstrap popovers for trending items 3 * @module trending-tooltip 4 * @version 1.0.0 5 */ 6 7class TrendingTooltip { 8 static popovers = new Map(); 9 10 static init() { 11 // Skip hover cards on touch/coarse-pointer devices - hover popovers hijack 12 // the first tap (showing the card instead of following the link) and can 13 // obscure the tapped item on small screens. 14 if (!this.supportsHover()) { 15 console.log('[TrendingTooltip] Hover not supported, skipping popovers'); 16 return; 17 } 18 19 const items = document.querySelectorAll('[data-trending-tooltip]'); 20 items.forEach(item => { 21 const existingPopover = bootstrap.Popover.getInstance(item); 22 if (existingPopover) { 23 existingPopover.dispose(); 24 this.popovers.delete(item); 25 } 26 27 const site = item.getAttribute('data-trending-site') || ''; 28 const description = item.getAttribute('data-trending-description') || ''; 29 const minutes = parseInt(item.getAttribute('data-trending-minutes') || '0'); 30 const origin = item.getAttribute('data-trending-origin') || ''; 31 const timeText = this.formatMinutesAgo(minutes); 32 33 const content = ` 34 <div class="d-flex flex-column gap-2"> 35 <div> 36 <div class="fw-bold fs-6 mb-1">${this.escapeHtml(site)}</div> 37 <div class="text-muted small">${timeText}</div> 38 </div> 39 <div class="small lh-base">${this.escapeHtml(description)}</div> 40 <div class="text-muted" style="font-size: 0.75rem;">${this.escapeHtml(origin)}</div> 41 </div> 42 `; 43 44 const popover = new bootstrap.Popover(item, { 45 trigger: 'hover', 46 placement: 'top', 47 html: true, 48 content: content, 49 container: 'body', 50 customClass: 'trending-popover', 51 delay: { show: 300, hide: 100 } 52 }); 53 54 this.popovers.set(item, popover); 55 }); 56 57 console.log('[TrendingTooltip] Initialized', this.popovers.size, 'tooltips'); 58 } 59 60 static supportsHover() { 61 return window.matchMedia('(hover: hover) and (pointer: fine)').matches; 62 } 63 64 static formatMinutesAgo(minutes) { 65 if (minutes < 1) return 'Just now'; 66 if (minutes === 1) return '1 minute ago'; 67 if (minutes < 60) return `${minutes} minutes ago`; 68 if (minutes < 120) return '1 hour ago'; 69 if (minutes < 1440) { 70 const hours = Math.floor(minutes / 60); 71 return `${hours} hours ago`; 72 } 73 const days = Math.floor(minutes / 1440); 74 return days === 1 ? '1 day ago' : `${days} days ago`; 75 } 76 77 static escapeHtml(text) { 78 const div = document.createElement('div'); 79 div.textContent = text; 80 return div.innerHTML; 81 } 82 83 static destroy() { 84 this.popovers.forEach((popover) => popover.dispose()); 85 this.popovers.clear(); 86 console.log('[TrendingTooltip] Destroyed'); 87 } 88} 89 90// Auto-initialize (modules are deferred, DOM is ready) 91TrendingTooltip.init(); 92 93// Export for other modules 94export { TrendingTooltip }; 95 96// Expose globally for debugging 97window.TrendingTooltip = TrendingTooltip;
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.