1/** 2 * Interviews & Podcasts â hover preview tooltips 3 * Shows a rounded-rectangle image preview when hovering an appearance link. 4 * Direct-image links render immediately; the 7min link pulls a live 5 * screenshot of the site via a screenshot service (warmed on load). 6 */ 7(function () { 8 'use strict'; 9 10 /** 11 * Build the ordered list of screenshot-service URLs to try for a site. 12 * Each returns the screenshot as an image directly, so it works as an <img src>. 13 */ 14 function screenshotCandidates(targetUrl) { 15 var enc = encodeURIComponent(targetUrl); 16 return [ 17 'https://s0.wp.com/mshots/v1/' + enc + '?w=600&h=450', 18 'https://api.microlink.io/?url=' + enc + '&screenshot=true&meta=false&embed=screenshot.url', 19 'https://image.thum.io/get/width/600/crop/900/' + targetUrl 20 ]; 21 } 22 23 /** 24 * Resolve the working image src for a link, trying fallbacks in order. 25 * Caches the resolved src on the element so later hovers are instant. 26 */ 27 function resolveSrc(link) { 28 if (link._previewResolved) { 29 return Promise.resolve(link._previewResolved); 30 } 31 if (link._previewPromise) { 32 return link._previewPromise; 33 } 34 35 // Prefer an explicit image; fall back to live screenshots when the link 36 // is a screenshot type (so the 7min preview still works if the local 37 // image is missing). 38 var candidates = []; 39 var explicit = link.getAttribute('data-preview-src'); 40 if (explicit) candidates.push(explicit); 41 if (link.getAttribute('data-preview-type') === 'screenshot') { 42 candidates = candidates.concat(screenshotCandidates(link.getAttribute('data-preview-url'))); 43 } 44 45 link._previewPromise = new Promise(function (resolve) { 46 var i = 0; 47 function tryNext() { 48 if (i >= candidates.length) { 49 resolve(candidates[0]); // give up; let the <img> show its own error 50 return; 51 } 52 var src = candidates[i++]; 53 var probe = new Image(); 54 probe.onload = function () { 55 link._previewResolved = src; 56 resolve(src); 57 }; 58 probe.onerror = tryNext; 59 probe.src = src; 60 } 61 tryNext(); 62 }); 63 return link._previewPromise; 64 } 65 66 function init() { 67 var links = document.querySelectorAll('.appearances-list a[data-preview]'); 68 if (!links.length) return; 69 70 // Single shared tooltip element. 71 var tip = document.createElement('div'); 72 tip.className = 'appearance-preview'; 73 tip.setAttribute('role', 'tooltip'); 74 tip.innerHTML = 75 '<div class="appearance-preview__frame">' + 76 '<img class="appearance-preview__img" alt="" decoding="async">' + 77 '<div class="appearance-preview__spinner" aria-hidden="true"></div>' + 78 '</div>'; 79 document.body.appendChild(tip); 80 81 var img = tip.querySelector('.appearance-preview__img'); 82 var activeLink = null; 83 var hideTimer = null; 84 85 function position(link) { 86 var rect = link.getBoundingClientRect(); 87 var tipRect = tip.getBoundingClientRect(); 88 var gap = 12; 89 90 // Default: to the right of the link, vertically centred on it. 91 var left = rect.right + window.scrollX + gap; 92 var top = rect.top + window.scrollY + (rect.height - tipRect.height) / 2; 93 94 // If it would overflow the right edge, flip to the left of the link. 95 var viewportRight = window.scrollX + document.documentElement.clientWidth; 96 if (left + tipRect.width > viewportRight - 12) { 97 var leftSide = rect.left + window.scrollX - tipRect.width - gap; 98 if (leftSide > window.scrollX + 12) left = leftSide; 99 else left = viewportRight - tipRect.width - 12; 100 } 101 102 // Keep within the viewport vertically. 103 var minTop = window.scrollY + 12; 104 var maxTop = window.scrollY + window.innerHeight - tipRect.height - 12; 105 if (top < minTop) top = minTop; 106 if (top > maxTop) top = Math.max(minTop, maxTop); 107 108 tip.style.left = left + 'px'; 109 tip.style.top = top + 'px'; 110 } 111 112 function show(link) { 113 activeLink = link; 114 img.classList.remove('is-loaded'); 115 116 resolveSrc(link).then(function (src) { 117 if (activeLink !== link) return; // hover moved on 118 if (img.src !== src) { 119 img.onload = function () { 120 if (activeLink === link) img.classList.add('is-loaded'); 121 }; 122 img.src = src; 123 } else { 124 img.classList.add('is-loaded'); 125 } 126 }); 127 128 // Make it measurable before positioning, then reveal. 129 tip.style.visibility = 'hidden'; 130 tip.classList.add('is-visible'); 131 position(link); 132 tip.style.visibility = ''; 133 } 134 135 function hide() { 136 activeLink = null; 137 tip.classList.remove('is-visible'); 138 } 139 140 links.forEach(function (link) { 141 link.addEventListener('mouseenter', function () { 142 if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } 143 show(link); 144 }); 145 link.addEventListener('mouseleave', function () { 146 hideTimer = setTimeout(hide, 100); 147 }); 148 link.addEventListener('focus', function () { show(link); }); 149 link.addEventListener('blur', hide); 150 }); 151 152 window.addEventListener('scroll', function () { 153 if (activeLink) position(activeLink); 154 }, { passive: true }); 155 156 // Warm all previews so the image is ready on first hover (especially the 157 // live screenshot, which needs the service to generate + cac
157he it). 158 if ('requestIdleCallback' in window) { 159 requestIdleCallback(function () { links.forEach(resolveSrc); }); 160 } else { 161 setTimeout(function () { links.forEach(resolveSrc); }, 1200); 162 } 163 } 164 165 if (document.readyState === 'loading') { 166 document.addEventListener('DOMContentLoaded', init); 167 } else { 168 init(); 169 } 170})();
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.