1(function () { 2 'use strict'; 3 4 var elements = document.querySelectorAll('[data-ypb-ranking]'); 5 if (!elements.length) return; 6 7 // Where the widget lives. Detected from this script's own <script src> 8 // so local and staging builds point at themselves; the constant is the 9 // fallback for host pages whose speed plugins hide that src (WP Rocket 10 // moves it to data-rocket-src, others inline or bundle the tag). With no 11 // fallback an undetected src left baseUrl empty and every iframe loaded 12 // from the blogger's own domain â their 404 page, sized like our widget. 13 var DEFAULT_BASE_URL = 'https://youpick.best'; 14 15 // How long the widget gets to report its height before we assume it did 16 // not start and add a plain link under the frame. 17 var FALLBACK_DELAY_MS = 10000; 18 19 var baseUrl = ''; 20 21 // Detect base URL from the script src. Strip the query string first â 22 // CMS plugins append cache busters (WordPress: ?ver=1.0.0), and replacing 23 // on the raw src glued the query onto the base URL, pointing every iframe 24 // at the homepage instead of the embed shell. 25 var scripts = document.getElementsByTagName('script'); 26 for (var i = 0; i < scripts.length; i++) { 27 var src = (scripts[i].src || '').split('?')[0]; 28 if (src.indexOf('/embed/v1/loader.js') !== -1) { 29 baseUrl = src.replace('/embed/v1/loader.js', ''); 30 break; 31 } 32 } 33 if (!baseUrl) baseUrl = DEFAULT_BASE_URL; 34 35 // Single delegating message listener for all iframes. The first resize 36 // message is also the proof the widget started, so it lifts the fallback. 37 var iframeMap = new Map(); 38 var liveIframes = new Set(); 39 window.addEventListener('message', function (event) { 40 if (!event.data || event.data.type !== 'ypb-resize' || typeof event.data.height !== 'number') return; 41 var iframe = iframeMap.get(event.source); 42 if (iframe) { 43 iframe.style.height = event.data.height + 'px'; 44 iframe.style.minHeight = ''; 45 liveIframes.add(iframe); 46 removeFallbackLink(iframe); 47 } 48 }); 49 50 // Reserve the widget's footprint before it loads (no layout shift) and 51 // make the reservation visible: a faint outlined card with a label. The 52 // reservation used to be transparent so light hosts saw no dark flash, 53 // but on a dark theme an empty transparent block reads as a black hole â 54 // exactly what a blogger saw when the iframe never arrived. Grey with 55 // alpha stays quiet on light and dark pages alike, and reads as "loading" 56 // rather than as a gap in the article if the widget never shows up. 57 // The host's own inline styles on the container are saved and put back 58 // when the iframe takes over. 59 var RESERVATION_STYLES = { 60 minHeight: '500px', 61 boxSizing: 'border-box', 62 display: 'flex', 63 alignItems: 'center', 64 justifyContent: 'center', 65 border: '1px solid rgba(128, 128, 128, 0.25)', 66 borderRadius: '12px', 67 background: 'rgba(128, 128, 128, 0.06)', 68 color: 'rgba(128, 128, 128, 0.9)', 69 font: '14px system-ui, -apple-system, sans-serif' 70 }; 71 var savedStyles = new Map(); 72 73 function reserveSpace(el) { 74 var saved = {}; 75 for (var prop in RESERVATION_STYLES) { 76 saved[prop] = el.style[prop]; 77 el.style[prop] = RESERVATION_STYLES[prop]; 78 } 79 savedStyles.set(el, saved); 80 el.textContent = 'Loading YouPickBest ranking\u2026'; 81 } 82 83 // Undo reserveSpace once the iframe takes over, so the container is the 84 // host's plain block again and the widget's card is the only visible frame. 85 function releaseSpace(el) { 86 el.innerHTML = ''; 87 var saved = savedStyles.get(el); 88 if (!saved) return; 89 for (var prop in saved) { 90 el.style[prop] = saved[prop]; 91 } 92 savedStyles.delete(el); 93 } 94 95 // If the widget has not reported its height FALLBACK_DELAY_MS after the 96 // iframe was created, it did not start â blocked frame, ad blocker, 97 // corporate proxy, whatever the cause. Add a plain link under the frame 98 // so the reader still reaches the ranking and the blogger sees the block 99 // is alive. The iframe stays: on a slow connection the widget may still 100 // arrive, and the resize listener then removes the link again, so the 101 // worst case is one extra line for a few seconds. 102 function scheduleFallbackLink(el, iframe, slug) { 103 window.setTimeout(function () { 104 if (liveIframes.has(iframe) || !iframe.parentNode) return; 105 var link = document.createElement('a'); 106 link.href = baseUrl + '/ranking/' + encodeURIComponent(slug); 107 link.target = '_blank'; 108 link.rel = 'noopener'; 109 link.setAttribute('data-ypb-fallback', ''); 110 link.textContent = 'Open this ranking on YouPickBest â'; 111 link.style.display = 'block'; 112 link.style.marginTop = '8px'; 113 el.appendChild(link); 114 }, FALLBACK_DELAY_MS); 115 } 116 117 function removeFallbackLink(iframe) { 118 var container = iframe.parentNode; 119 if (!container) return; 120 var link = container.querySelector('[data-ypb-fallback]'); 121 if (link) container.removeChild(link); 122 } 123 124 function createIframe(el) { 125 var slug = el.getAttribute('data-ypb-ranking'); 126 if (!slug) return; 127 128 var utmSource = el.getAttribute('data-ypb-utm-source') || ''; 129 // The referrer param keys the widget's per-page audience pool, so it 130 // must be the page hosting the widget. document.referrer is where the 131 // visitor came FROM (search engine, social) â using it first split one 132 // post's votes into per-traffic-source pools. 133 var referrer = encodeURIComponent(window.location.href || document.referrer); 134 var params = '?referrer=' + referrer; 135 if (utmSource) params += '&utm_source=' + encodeURIComponent(utmSource); 136 137 var iframe = document.createElement('iframe'); 138 iframe.src = baseUrl + '/embed/ranking/' + encodeURIComponent(slug) + params; 139 iframe.style.width = '100%'; 140 iframe.style.minHeight = '500px'; 141 iframe.style.border = 'none'; 142 iframe.style.borderRadius = '12px'; 143 // No color-scheme: forcing one makes the browser paint an opaque 144 // backdrop behind the transparent widget (white via mismatch, or black 145 // when forced dark) on the host page. Transparent background + no 146 // color-scheme lets the host show through; the card has its own dark fill. 147 iframe.style.background = 'transparent'; 148 // No loading="lazy": the IntersectionObserver below is the only lazy 149 // gate. Stacking the browser's own deferral on top re-delays the load 150 // by its per-browser distance heuristic (Firefox's is smaller than our 151 // preload margin), letting a scrolling reader catch the widget half-loaded. 152 iframe.setAttribute('allow', 'clipboard-write'); 153 iframe.setAttribute('title', 'YouPickBest Ranking Poll'); 154 155 releaseSpace(el); 156 el.appendChild(iframe); 157 scheduleFallbackLink(el, iframe, slug); 158 159 // Register the iframe's window for resize messages immediately, not on 160 // its load event. The WindowProxy returned by contentWindow is stable 161 // across the iframe's navigation, so it already equals the event.source 162 // the widget posts from. Registering on load instead raced the widget's 163 // first resize message â if that message arrived first it was dropped, 164 // leaving the iframe stuck at min-height (clipped widget in Safari and 165 // some Brave builds). The load listener stays as a belt-and-suspenders. 166 if (iframe.contentWindow) { 167 iframeMap.set(iframe.contentWindow, iframe); 168 } 169 iframe.addEventListener('load', function () { 170 if (iframe.contentWindow) { 171 iframeMap.set(iframe.contentWindow, iframe); 172 } 173 }); 174 } 175 176 // Lazy loading with IntersectionObserver 177 if ('IntersectionObserver' in window) { 178 var observer = new IntersectionObserver(function (entries) { 179 for (var i = 0; i < entries.length; i++) { 180 if (entries[i].isIntersecting) { 181 createIframe(entries[i].target); 182 observer.unobserve(entries[i].target); 183 } 184 } 185 // 1500px â 1.5â2 screens of runway: the widget must be fully rendered 186 // by the time a reader scrolls to it (a visible pop-in reads as a 187 // glitchy foreign element), while readers who never scroll down still 188 // cost no embed request. 200px started the load too late to win that race. 189 }, { rootMargin: '1500px' }); 190 191 for (var j = 0; j < elements.length; j++) { 192 // No slug, nothing to load: don't promise a widget that never comes. 193 if (elements[j].getAttribute('data-ypb-ranking')) { 194 reserveSpace(elements[j]); 195 } 196 observer.observe(elements[j]); 197 } 198 } else { 199 // Fallback: load all immediately 200 for (var k = 0; k < elements.length; k++) { 201 createIframe(elements[k]); 202 } 203 } 204})();
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.