1function getViewportHeight() { 2 return window.visualViewport 3 ? window.visualViewport.height 4 : window.innerHeight; 5} 6 7function isMobileViewport() { 8 return window.innerWidth < 768; 9} 10 11const MOBILE_RESIZE_EPSILON_PX = 1; 12 13// Freeze viewport-based sizing to the largest observed viewport. 14let FROZEN_LARGEST_VIEWPORT_HEIGHT = getViewportHeight(); 15 16function getFrozenLargestViewportHeight() { 17 const currentHeight = getViewportHeight(); 18 if (currentHeight > FROZEN_LARGEST_VIEWPORT_HEIGHT) { 19 FROZEN_LARGEST_VIEWPORT_HEIGHT = currentHeight; 20 } 21 return FROZEN_LARGEST_VIEWPORT_HEIGHT; 22} 23 24function getStableViewportHeightCss(offset = 0) { 25 if (!isMobileViewport()) { 26 return Math.max(0, getViewportHeight() - offset) + "px"; 27 } 28 29 return Math.max(0, getFrozenLargestViewportHeight() - offset) + "px"; 30} 31 32// Origins allowed to drive iframe sizing/scroll. Add new embed hosts here â this is the 33// single source of truth for both the message-origin check and the iframe-src checks below. 34// The S3 media bucket stays a pattern because the region lives in the subdomain. 35const EMBED_ORIGINS = [ 36 "https://vallenato-media.cfr.org", 37 "https://static.cfr.org", 38]; 39function isAllowedEmbedOrigin(origin) { 40 return ( 41 EMBED_ORIGINS.includes(origin) || 42 (origin.startsWith("https://cfr-vallenato-media.s3.") && origin.endsWith(".amazonaws.com")) 43 ); 44} 45function isAllowedEmbedSrc(src) { 46 try { 47 return isAllowedEmbedOrigin(new URL(src).origin); 48 } catch (e) { 49 return false; 50 } 51} 52 53// This listener correctly handles multiple iframes from different sources. 54window.addEventListener("message", function (event) { 55 // -- Handle your ai2html graphics from either the S3 or custom domain -- 56 if (isAllowedEmbedOrigin(event.origin)) { 57 if ( 58 event.data && 59 event.data.type !== "scrollableHeight" && 60 event.data.type !== "dwHeight" && 61 typeof event.data.height !== "undefined" 62 ) { 63 const iframes = document.querySelectorAll("iframe"); 64 65 for (let iframe of iframes) { 66 if (iframe.contentWindow === event.source) { 67 if (iframe.dataset.scrollyReady) break; 68 iframe.style.minHeight = "0"; 69 iframe.style.height = event.data.height + "px"; 70 break; 71 } 72 } 73 } 74 75 if (event.data.type === "imageAlt" && event.data.alt) { 76 const iframes = document.querySelectorAll("iframe"); 77 for (let iframe of iframes) { 78 if (iframe.contentWindow === event.source) { 79 iframe.setAttribute("role", "img"); 80 iframe.setAttribute("aria-label", event.data.alt); 81 if (event.data.title) { 82 iframe.setAttribute("title", event.data.title); 83 } 84 if (event.data.description) { 85 iframe.setAttribute("aria-description", event.data.description); 86 } 87 break; 88 } 89 } 90 } 91 92 if (event.data.type === "scrollIntoView") { 93 const iframes = document.querySelectorAll("iframe"); 94 for (let iframe of iframes) { 95 if (iframe.contentWindow === event.source) { 96 iframe.scrollIntoView({ behavior: "smooth", block: "start" }); 97 break; 98 } 99 } 100 return; 101 } 102 103 if (event.data.type === "wheelForward") { 104 window.scrollBy(0, event.data.deltaY); 105 return; 106 } 107 108 if (event.data.type === "scrollableHeight") { 109 const iframes = document.querySelectorAll("iframe"); 110 for (let iframe of iframes) { 111 if (iframe.contentWindow === event.source) { 112 iframe.dataset.scrollyReady = "true"; 113 114 const scrollableHeight = Number(event.data.height); 115 if (!Number.isFinite(scrollableHeight) || scrollableHeight <= 0) { 116 return; 117 } 118 119 const isTestPointer = iframe.src.includes("sandbox"); 120 121 const track = iframe.parentNode; 122 track.style.position = "relative"; 123 const nextTrackHeight = Math.round(scrollableHeight); 124 125 iframe.dataset.scrollyTrackHeight = String(nextTrackHeight); 126 track.style.height = nextTrackHeight + "px"; 127 128 // For an already-initialized scrolly iframe, only refresh height contract. 129 if (iframe.dataset.scrollyInitialized) break; 130 131 if (!isTestPointer) { 132 iframe.style.pointerEvents = "none"; 133 } else { 134 iframe.addEventListener("wheel", (e) => e.preventDefault(), { 135 passive: false, 136 }); 137 iframe.addEventListener("touchmove", (e) => e.preventDefault(), { 138 passive: false, 139 }); 140 } 141 142 iframe.style.scrollbarWidth = "none"; 143 144 let ticking = false;
145 let scrollTimer; 146 const updateLayout = () => { 147 const header = document.querySelector(".fixed.top-0.z-50"); 148 const offset = header ? header.getBoundingClientRect().height : 0; 149 iframe.style.top = offset + "px"; 150 iframe.style.height = getStableViewportHeightCss(offset); 151 return offset; 152 }; 153 let headerOffset = updateLayout(); 154 155 iframe.dataset.scrollyInitialized = "true"; 156 157 iframe.style.position = "sticky"; 158 iframe.style.width = "100%"; 159 iframe.style.minHeight = "0"; 160 iframe.setAttribute("scrolling", "no"); 161 162 const onScroll = () => { 163 if (!isTestPointer) { 164 iframe.style.pointerEvents = "none"; 165 } 166 clearTimeout(scrollTimer); 167 168 if (ticking) return; 169 ticking = true; 170 requestAnimationFrame(() => { 171 const rect = track.getBoundingClientRect(); 172 const viewportHeight = 173 headerOffset + iframe.getBoundingClientRect().height; 174 const scrollDistance = Math.max(0, headerOffset - rect.top); 175 const maxScroll = Math.max(1, rect.height - viewportHeight); 176 const progress = Math.min( 177 1, 178 Math.max(0, scrollDistance / maxScroll), 179 ); 180 181 iframe.dataset.scrollProgress = progress; 182 iframe.contentWindow.postMessage( 183 { 184 type: "scrollDistance", 185 scrollDistance, 186 }, 187 "*", 188 ); 189 ticking = false; 190 }); 191 }; 192 193 let layoutRafId = 0; 194 const syncLayoutAndScroll = () => { 195 if (layoutRafId) return; 196 layoutRafId = requestAnimationFrame(() => { 197 layoutRafId = 0; 198 headerOffset = updateLayout(); 199 onScroll(); 200 }); 201 }; 202 203 let lastMobileMode = isMobileViewport(); 204 let lastViewportHeight = getViewportHeight(); 205 const onViewportResize = () => { 206 const mobileMode = isMobileViewport(); 207 const viewportHeight = getViewportHeight(); 208 const viewportGrew = 209 viewportHeight > lastViewportHeight + MOBILE_RESIZE_EPSILON_PX; 210 211 lastViewportHeight = viewportHeight; 212 213 if (mobileMode !== lastMobileMode) { 214 lastMobileMode = mobileMode; 215 syncLayoutAndScroll(); 216 return; 217 } 218 219 if (!mobileMode) { 220 syncLayoutAndScroll(); 221 return; 222 } 223 224 // On mobile, only grow when the usable viewport gets larger. 225 if (viewportGrew) { 226 syncLayoutAndScroll(); 227 } 228 }; 229 230 window.addEventListener("resize", onViewportResize); 231 if (window.visualViewport) { 232 window.visualViewport.addEventListener("resize", onViewportResize); 233 } 234 235 window.addEventListener("scroll", onScroll, { passive: true }); 236 onScroll(); 237 break; 238 } 239 } 240 return; 241 } 242 243 if (event.data.type === "dwHeight") { 244 const iframes = document.querySelectorAll("iframe"); 245 for (let iframe of iframes) { 246 if (iframe.contentWindow === event.source) { 247 iframe.style.minHeight = "0"; 248 iframe.style.height = event.data.height + "px"; 249 break; 250 } 251 } 252 return; 253 } 254 255 return; 256 } 257 258 if (event.origin === "https://datawrapper.dwcdn.net") { 259 if (event.data["datawrapper-height"] !== undefined) { 260 const iframes = document.querySelectorAll("iframe"); 261 for (const chartId in event.data["datawrapper-height"]) { 262 for (let iframe of iframes) { 263 if (iframe.contentWindow === event.source) { 264 iframe.style.height = 265 event.data["datawrapper-height"][chartId] + "px"; 266 267 const url = new URL(iframe.src); 268 const maxWidth = 269 url.searchParams.get("maxwidth") || 270 url.searchParams.get("maxWidth") || 271 url.searchParams.get("MAXWIDTH"); 272 if (maxWidth) { 273 const hasUnit = /[a-z%]/i.test(maxWidth); 274 iframe.style.maxWidth = hasUnit ? maxWidth : maxWidth + "px"; 275 iframe.style.width = "100%"; 276 iframe.style.margin = "0 auto"; 277 iframe.style.display = "block"; 278 iframe.style.overflow = "hidden"; 279 } 280 } 281 } 282 } 283 } 284 return; 285 } 286}); 287 288window.addEventListener("load", () => { 289 const iframes = document.querySelectorAll("iframe"); 290 for (let iframe of iframes) { 291 if (iframe.src && iframe.src.includes("scrolly=true")) {
292 iframe.style.height = getStableViewportHeightCss(0); 293 } 294 } 295 296 iframes.forEach((iframe) => { 297 if (isAllowedEmbedSrc(iframe.src)) { 298 iframe.addEventListener("load", function () { 299 setTimeout(() => { 300 try { 301 iframe.contentWindow.dispatchEvent(new Event("resize")); 302 } catch (e) {} 303 }, 100); 304 }); 305 } 306 }); 307}); 308 309const observer = new IntersectionObserver( 310 (entries) => { 311 for (const entry of entries) { 312 if (entry.isIntersecting) { 313 try { 314 entry.target.contentWindow.dispatchEvent(new Event("resize")); 315 } catch (e) {} 316 } 317 } 318 }, 319 { threshold: 0.1 }, 320); 321 322document.querySelectorAll("iframe").forEach((iframe) => { 323 if (isAllowedEmbedSrc(iframe.src)) { 324 observer.observe(iframe); 325 } 326}); 327 328document.addEventListener("visibilitychange", () => { 329 if (document.visibilityState === "visible") { 330 const iframes = document.querySelectorAll("iframe"); 331 for (let iframe of iframes) { 332 if (isAllowedEmbedSrc(iframe.src)) { 333 if (iframe.offsetHeight === 0 || !iframe.contentWindow) { 334 iframe.src = iframe.src; 335 } 336 } 337 } 338 } 339}); 340 341function broadcastParentScroll() { 342 const iframes = document.querySelectorAll("iframe"); 343 for (let iframe of iframes) { 344 try { 345 if (isAllowedEmbedSrc(iframe.src)) { 346 if (iframe.dataset.scrollyInitialized) continue; 347 const origin = new URL(iframe.src).origin; 348 iframe.contentWindow.postMessage( 349 { 350 type: "parentScrollInfo", 351 viewportHeight: window.innerHeight, 352 iframeViewportTop: iframe.getBoundingClientRect().top, 353 }, 354 origin 355 ); 356 } 357 } catch (e) {} 358 } 359} 360 361window.addEventListener("scroll", broadcastParentScroll, { passive: true }); 362window.addEventListener("resize", broadcastParentScroll, { passive: true });
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.