PageSourceSearch

https://vallenato-media.cfr.org/vallenato/js/iframe-resizer-vallenato.js

js cfr.org collected 2026-09-24 07:04:45 UTC 11,343 bytes, 362 lines download raw bytes

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.