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.