PageSourceSearch

https://votertools.voteidaho.gov/TEDElectionLink/_content/TEDxCommonUI/js/tedDashboardSwap.enzm2kn3eh.js

js voteidaho.gov collected 2026-10-02 07:51:05 UTC 14,296 bytes, 236 lines download raw bytes

1// Sidebar dashboard fast-switch: swaps the dashboard BODY in place instead of navigating.
2//
3// The left-hand sidebar (app_menu MenuType='sidebar' rows) links sibling dashboards as ordinary
4// /{module}/dashboard/view/{code} anchors — or, on the chromeless embed, /embed/{module}/dashboard/{code}
5// anchors carrying a per-board ?token=. A full (enhanced) navigation re-renders the whole page
6// server-side — layout chrome queries included — which reads as slow. When the current page carries a
7// [data-ted-dashboard-swap] container (DashboardViewContent renders it under static SSR — the ElectionLink
8// page AND the embed), a click on a SAME-MODULE dashboard link is intercepted here: fetch the board's HTML
9// fragment from /api/v1/dashboards/html/{code} (the link's query, token included, forwarded verbatim so the
10// board renders under its own credential) and swap it into the container. The page's MutationObserver islands do
11// the rest — tileInterop stops the outgoing tiles' polls / disposes their charts on removal and hydrates
12// the incoming ones, tedDashboardControls wires the incoming control bar.
13//
14// PREFETCH + fragment cache (mirrors tileInterop's preloadSwitchOptions one level up): once a board has
15// settled, the same-module sidebar dashboards are background-fetched into a client cache, so a click
16// paints instantly with no request in the way. A hit older than FRESH_MS is revalidated after paint and
17// re-applied only when the markup actually changed — and staleness self-heals regardless, because the
18// swapped-in tiles' poll islands own refresh. The server keeps its own SWR fragment cache
19// (DashboardFragmentRenderer), so the prefetch burst is cheap across viewers.
20//
21// Contracts this relies on (keep in sync):
22//   * DashboardViewContent.razor always wraps DashboardViewer in the swap div but renders the
23//     data-ted-dashboard-swap attribute (what the selector matches) under static SSR only — which covers
24//     the ElectionLink page AND the embed. No armed container → this island does nothing (interactive
25//     TEDMatrix, other pages).
26//   * The dashboard-fragment endpoint binds an embed token to the board it names (DashboardViewer checks
27//     tokenCode == the rendered code), so forwarding the link's ?token= can only ever render that board.
28//   * DashboardHtmlEndpoint returns exactly the DashboardViewer markup for a code; its query string is
29//     passed through verbatim so ambient params behave byte-identically to a real page load.
30//   * tileInterop.js loads BEFORE this file: its capture-phase click listener applies the tile loading
31//     treatment (markNavPending) before we preventDefault, so the switch shows the same immediate
32//     feedback a navigation does.
33//   * History entries this island creates are marked {tedDashSwap:true}; popstate is handled ONLY for
34//     marked entries — unmarked traversals stay Blazor's (enhanced nav) to resolve.
35(function () {
36    "use strict";
37    if (window.tedDashboardSwap) return;
38    window.tedDashboardSwap = true;
39
40    var CONTAINER_SEL = "[data-ted-dashboard-swap]";
41    var VIEW_ROUTE = /^\/([^/]+)\/dashboard\/view\/([^/?#]+)\/?$/;
42    // The chromeless embed surface: /embed/{module}/dashboard/{code}. Same {module, code} shape, so the
43    // swap treats an embed page and its sidebar links exactly like view URLs — the only difference is the
44    // link's ?token= (a per-board embed token), which rides through to the fragment fetch verbatim
45    // (see fetchFragment) so the endpoint renders that board under its token. Kept as a SEPARATE pattern
46    // so the ElectionLink view path is byte-for-byte unchanged.
47    var EMBED_ROUTE = /^\/embed\/([^/]+)\/dashboard\/([^/?#]+)\/?$/;
48    var FRESH_MS = 15000;      // a cached fragment younger than this is applied without revalidation
49    var CACHE_MAX = 12;        // LRU cap — a sidebar is a handful of boards; bound memory regardless
50    var PREFETCH_DELAY = 800;  // let the current board's tiles hydrate before warming siblings
51
52    function container() { return document.querySelector(CONTAINER_SEL); }
53
54    // The app's base path from <base href> (AppBasePath — e.g. "/TEDElectionLink"), "" at root. The
55    // route lives BELOW it (UsePathBase strips it server-side), so app-relative parsing must too.
56    function basePath() {
57        var p = new URL(document.baseURI).pathname;
58        return p.endsWith("/") ? p.slice(0, -1) : p;
59    }
60
61    // {module, code} from a Location/URL's pathname, or null when it isn't a dashboard view/embed URL.
62    function parseView(url) {
63        var path = url.pathname;
64        var base = basePath();
65        if (base) {
66            if (!path.startsWith(base + "/")) return null;
67            path = path.slice(base.length);
68        }
69        var m = VIEW_ROUTE.exec(path) || EMBED_ROUTE.exec(path);
70        return m ? { module: decodeURIComponent(m[1]), code: decodeURIComponent(m[2]) } : null;
71    }
72
73    // ── fragment cache ──────────────────────────────────────────────────────────────────────────────
74    // key = pathname+search (the page identity the fragment renders). Map iterates in insertion order,
75    // so re-inserting on every touch makes deleting the first key an LRU eviction.
76    var frags = new Map();     // key -> { html, at }
77    var inflight = new Map();  // key -> Promise<string> (dedup: hover + prefetch + click share one fetch)
78
79    function keyFor(url) { return url.pathname + url.search; }
80
81    function remember(key, html) {
82        frags.delete(key);
83        frags.set(key, { html: html, at: Date.now() });
84        while (frags.size > CACHE_MAX) frags.delete(frags.keys().next().value);
85    }
86
87    // Fetch a board's fragment (deduped) and remember it. Resolved against <base> so the request stays
88    // under the app's path base (a proxy that routes only /{AppBasePath}/* to this app still sees it;
89    // UsePathBase strips the prefix server-side).
90    function fetchFragment(url) {
91        var key = keyFor(url);
92        var pending = inflight.get(key);
93        if (pending) return pending;
94        var fragUrl = new URL("api/v1/dashboards/html/" + encodeURIComponent(parseView(url).code) + url.search, document.baseURI);
95        pending = fetch(fragUrl, { credentials: "same-origin", headers: { accept: "text/html" } })
96            .then(function (res) {
97                if (!res.ok) throw new Error("fragment " + res.status);
98                return res.text();
99            })
100            .then(
101                function (html) { inflight.delete(key); remember(key, html); return html; },
102                function (err) { inflight.delete(key); throw err; });
103        inflight.set(key, pending);
104        return pending;
105    }
106
107    // Warm the cache for every same-module dashboard link on the page (the sidebar) — the shown board
108    // included, so Back re-enters through the cache (the live DOM can't be reused: chart hydration
109    // consumes the ECharts init blobs). Best-effort: failures are ignored (the click path fetches live
110    // and falls back to real navigation).
111    function prefetchSiblings() {
112        if (!container()) return;
113        if (navigator.connection && navigator.connection.saveData) return;
114        var current = parseView(location);
115        if (!current) return;
116        document.querySelectorAll("a[href]").forEach(function (a) {
117            var url;
118            try { url = new URL(a.href, location.href); } catch (e) { return; }
119            if (url.origin !== location.origin) return;
120            var target = parseView(url);
121            if (!target || target.module !== current.module) return;
122            if (frags.has(keyFor(url))) return;
123            fetchFragment(url).catch(function () { /* best-effort */ });
124        });
125    }
126
127    function schedulePrefetch() { setTimeout(prefetchSiblings, PREFETCH_DELAY); }
128
129    // ── swapping ────────────────────────────────────────────────────────────────────────────────────
130
131    // Newest click wins: a stale fetch that resolves after a newer switch must not clobber it.
132    var seq = 0;
133
134    // Put a fragment on screen (+ history/highlight bookkeeping when it's a user action, not a
135    // revalidation). Re-queries the container — an enhanced nav that raced us may have replaced the page.
136    function applyFragment(html, url, push) {
137        var host = container();
138        if (!host) return;
139        if (push) {
140            // Mark the CURRENT entry too (merge — Blazor keeps scroll state in history.state), so
141            // Back onto the entry we started from is recognized as ours and swaps in place.
142            if (!history.state || !history.state.tedDashSwap)
143                history.replaceState(Object.assign({}, history.state || {}, { tedDashSwap: true }), "");
144            history.pushState({ tedDashSwap: true }, "", url.pathname + url.search);
145        }
146        // MutationObservers: old tiles stop/dispose, new tiles hydrate, and TedToolbar.js re-mirrors the
147        // incoming board's carriers (tab title, "Data as of") onto the page chrome.
148        host.innerHTML = html;
149        syncActive(url);
150        schedulePrefetch();      // the new board's siblings may not all be warm yet
151    }
152
153    function swapTo(url, push) {
154        var mySeq = ++seq;
155        var hit = frags.get(keyFor(url));
156        if (hit) {
157            applyFragment(hit.html, url, push);
158            // Stale-while-revalidate: repaint only if the fresh markup actually differs (identical data
159            // re-rendering would just flash the charts; the tile polls keep the board live either way).
160            if (Date.now() - hit.at > FRESH_MS) {
161                fetchFragment(url)
162                    .then(function (html) { if (mySeq === seq && html !== hit.html) applyFragment(html, url, false); })
163                    .catch(function () { /* cached content is already on screen */ });
164            }
165            return;
166        }
167        fetchFragment(url)
168            .then(function (html) { if (mySeq === seq) applyFragment(html, url, push); })
169            .catch(function () {
170                // Endpoint missing/erroring: fall back to the real navigation the click meant.
171                location.assign(url.pathname + url.search);
172            });
173    }
174
175    // Move the nav highlight the way NavLink would have: every same-module dashboard link tracks whether
176    // it points at the now-shown code. Class name is the layout's (els-*) by default; another host layout
177    // can override it via data-ted-dashboard-swap="its-active-class".
178    function syncActive(url) {
179        var host = container();
180        var activeClass = (host && host.getAttribute("data-ted-dashboard-swap")) || "els-active";
181        var current = parseView(url);
182        if (!current) return;
183        document.querySelectorAll("a[href]").forEach(function (a) {
184            var target;
185            try { target = parseView(new URL(a.href, location.href)); } catch (e) { return; }
186            if (!target || target.module !== current.module) return;
187            var isActive = target.code === current.code;
188            a.classList.toggle(activeClass, isActive);
189            if (isActive) a.setAttribute("aria-current", "page");
190            else a.removeAttribute("aria-current");
191        });
192    }
193
194    // Same eligibility a router uses (mirrors tileInterop.isNavClick), PLUS: a swap container is on the
195    // page, both the current URL and the link are dashboard views, and the module matches (a cross-module
196    // link changes the layout chrome — that must stay a real navigation).
197    document.addEventListener("click", function (e) {
198        if (e.defaultPrevented || e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
199        var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
200        if (!a || (a.target && a.target !== "_self") || a.hasAttribute("download")) return;
201        if (!container()) return;
202
203        var url;
204        try { url = new URL(a.href, location.href); } catch (err) { return; }
205        if (url.origin !== location.origin) return;
206
207        var from = parseView(location);
208        var to = parseView(url);
209        if (!from || !to || from.module !== to.module) return;
210        if (url.pathname === location.pathname && url.search === location.search) return; // already shown
211
212        e.preventDefault();   // ours — keep Blazor's enhanced nav out of it
213        swapTo(url, true);
214    }, true);
215
216    // Back/Forward across entries THIS island created: swap without pushing (instant when the board is
217    // still cached). Anything unmarked is a traversal Blazor (or the browser) owns — don't touch it.
218    window.addEventListener("popstate", function (ev) {
219        if (!ev.state || !ev.state.tedDashSwap) return;
220        if (!container() || !parseView(location)) return;
221        swapTo(new URL(location.href), false);
222    });
223
224    // On arrival: set the nav highlight from the CURRENT url (the same {module,code} match a swap does),
225    // then warm the sidebar's siblings. NavLink's server-rendered ActiveClass can't carry the highlight
226    // here — the embed's sidebar hrefs carry a ?token= (plus forwarded pins) and NavLink's matching
227    // includes the query, so the shown board's own link usually fails to match on first paint. syncActive
228    // keys off {module,code} only, so it lights the right link immediately and identically to a post-swap
229    // highlight. Guarded on the swap container so this stays a no-op anywhere the island isn't armed.
230    function onArrival() {
231        if (container()) syncActive(new URL(location.href));
232        schedulePrefetch();
233    }
234    if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", onArrival);
235    else onArrival();
236})();

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.