1// Flight summary â progressive enhancement for the stage carousel and drill-downs. 2// The server renders EVERY drill-down open (that is what crawlers index); this layer 3// collapses them, wires card clicks, and drives the carousel scroll + minimap window. 4// Same file on the interactive page and the stored public page. 5(function () { 6 "use strict"; 7 8 // Explore accordion: one panel open at a time; the panel marked data-fx-default 9 // (the narrative) opens on load. Server renders all panels open for crawlers. 10 function initExplore(root) { 11 var cards = root.querySelectorAll("[data-fx-card]"); 12 if (!cards.length) return; 13 var panels = {}; 14 root.querySelectorAll("[data-fx-panel]").forEach(function (p) { 15 panels[p.getAttribute("data-fx-panel")] = p; 16 }); 17 var openKey = null; 18 19 function setOpen(key) { 20 if (openKey !== null && panels[openKey]) { 21 panels[openKey].classList.remove("open"); 22 var prev = root.querySelector('[data-fx-card="' + openKey + '"]'); 23 if (prev) { prev.classList.remove("open"); prev.setAttribute("aria-expanded", "false"); } 24 } 25 openKey = key; 26 if (key !== null && panels[key]) { 27 panels[key].classList.add("open"); 28 var card = root.querySelector('[data-fx-card="' + key + '"]'); 29 if (card) { card.classList.add("open"); card.setAttribute("aria-expanded", "true"); } 30 } 31 } 32 33 cards.forEach(function (card) { 34 card.addEventListener("click", function () { 35 var key = card.getAttribute("data-fx-card"); 36 setOpen(openKey === key ? null : key); 37 }); 38 }); 39 root.querySelectorAll("[data-fx-close]").forEach(function (btn) { 40 btn.hidden = false; 41 btn.addEventListener("click", function () { setOpen(null); }); 42 }); 43 44 var def = root.querySelector("[data-fx-default]"); 45 if (def) setOpen(def.getAttribute("data-fx-panel")); 46 } 47 48 function init(root) { 49 // Bind once per root. On the stored page this runs at load; in the app the same 50 // root is re-rendered by Blazor as new telemetry lands, and re-binding on every 51 // tick would stack duplicate handlers. 52 if (root.dataset.fsInit) return; 53 root.dataset.fsInit = "1"; 54 55 root.classList.add("js"); 56 initExplore(root); 57 58 var track = root.querySelector('[data-fs="track"]'); 59 if (!track) return; 60 61 // Looked up live, never captured. A live flight GAINS stage cards as it flies, so 62 // a map built at bind time would not know about the ones that arrive later. 63 function panelFor(idx) { return root.querySelector('[data-fs-panel="' + idx + '"]'); } 64 function cardFor(idx) { return root.querySelector('[data-fs-card="' + idx + '"]'); } 65 var openIdx = null; 66 67 function setOpen(idx) { 68 if (openIdx !== null) { 69 var open = panelFor(openIdx); 70 if (open) open.classList.remove("open"); 71 var prev = cardFor(openIdx); 72 if (prev) { prev.classList.remove("open"); prev.setAttribute("aria-expanded", "false"); } 73 } 74 openIdx = idx; 75 if (idx !== null) { 76 var panel = panelFor(idx); 77 if (panel) panel.classList.add("open"); 78 var card = cardFor(idx); 79 if (card) { card.classList.add("open"); card.setAttribute("aria-expanded", "true"); } 80 } 81 } 82 83 // Delegated, for the same reason the lookups are live: a card added to a flight in 84 // progress works without anything having to rebind. 85 // A link elsewhere on the page into one stage - "What held you back" names the 86 // stage that cost the points. Open it, bring its card into the strip and the 87 // strip into view, and pulse the card so the eye lands on the right one. 88 function reveal(idx) { 89 setOpen(idx); 90 var card = cardFor(idx); 91 if (!card) return; 92 var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 93 track.scrollTo({ left: card.offsetLeft - (track.clientWidth - card.offsetWidth) / 2, behavior: reduce ? "auto" : "smooth" }); 94 card.scrollIntoView({ block: "center", behavior: reduce ? "auto" : "smooth" }); 95 card.classList.remove("hit"); 96 void card.offsetWidth; 97 card.classList.add("hit"); 98 } 99 100 root.addEventListener("click", function (ev) { 101 var link = ev.target.closest("[data-fs-open]"); 102 if (link && root.contains(link)) { ev.preventDefault(); reveal(link.getAttribute("data-fs-open")); return; } 103 var close = ev.target.closest("[data-fs-close]"); 104 if (close && root.contains(close)) { setOpen(null); return; } 105 var card = ev.target.closest("[data-fs-card]"); 106 if (!card || !root.contains(card)) return; 107 var idx = card.getAttribute("data-fs-card"); 108 setOpen(openIdx === idx ? null : idx); 109 }); 110 root.querySelectorAll("[data-fs-close]").forEach(function (btn) { btn.hidden = false; }); 111 112 // ---- carousel scroll + counter + minimap window ---- 113 // Counted live, for the same reason the panels are looked up live: a flight in the
114 // air gains stage cards, so a count taken once at bind time goes stale. This also 115 // replaces the "var cards" that used to be declared up in the drill-down block - 116 // delegating those clicks removed the declaration and left the carousel below 117 // referencing a variable that no longer existed, which threw and killed the 118 // scroller outright. 119 function stageCards() { return root.querySelectorAll("[data-fs-card]"); } 120 121 var tools = root.querySelector(".story-tools"); 122 if (tools) tools.hidden = false; 123 var counter = root.querySelector('[data-fs="counter"]'); 124 var mmWindow = root.querySelector('[data-fs="window"]'); 125 if (mmWindow) mmWindow.hidden = false; 126 var segs = root.querySelectorAll("[data-fs-seg]"); 127 var weights = [], total = 0, cum = [0]; 128 segs.forEach(function (seg) { 129 var w = parseFloat(seg.style.flexGrow || "1"); 130 weights.push(w); total += w; cum.push(total); 131 }); 132 133 function metrics() { 134 var first = track.firstElementChild; 135 if (!first) return { step: 1, per: 1 }; 136 var step = first.getBoundingClientRect().width + 10; 137 var per = Math.max(1, Math.round(track.clientWidth / step)); 138 return { step: step, per: per }; 139 } 140 141 function sync() { 142 var m = metrics(); 143 var count = stageCards().length; 144 var first = Math.round(track.scrollLeft / m.step); 145 first = Math.max(0, Math.min(first, count - m.per)); 146 var last = Math.min(first + m.per - 1, count - 1); 147 if (counter) counter.textContent = (first + 1) + "â" + (last + 1) + " of " + count; 148 if (mmWindow && total > 0) { 149 mmWindow.style.left = (cum[first] / total * 100) + "%"; 150 mmWindow.style.width = ((cum[last + 1] - cum[first]) / total * 100) + "%"; 151 } 152 } 153 154 function scrollBy(dir) { 155 var m = metrics(); 156 track.scrollLeft += dir * m.per * m.step; 157 } 158 159 var prev = root.querySelector('[data-fs="prev"]'); 160 var next = root.querySelector('[data-fs="next"]'); 161 if (prev) prev.addEventListener("click", function () { scrollBy(-1); }); 162 if (next) next.addEventListener("click", function () { scrollBy(1); }); 163 segs.forEach(function (seg, i) { 164 seg.addEventListener("click", function () { 165 var m = metrics(); 166 track.scrollLeft = Math.min(i, stageCards().length - m.per) * m.step; 167 }); 168 }); 169 170 // Drag the minimap. It is shaped like a scrollbar thumb, so people try to grab it; 171 // the window itself stays pointer-events:none and the BAR takes the gesture, which 172 // means a press anywhere on it also jumps there, exactly as a scrollbar does. 173 // 174 // Pointer events cover mouse, touch and pen in one path. While dragging, this is 175 // the only thing writing scrollLeft - sync() reads it back and moves the window, so 176 // the two follow rather than fight. 177 var bar = root.querySelector(".mm-bar"); 178 if (bar) { 179 var dragging = false; 180 181 function scrollToPointer(ev) { 182 var rect = bar.getBoundingClientRect(); 183 if (rect.width <= 0) return; 184 var frac = (ev.clientX - rect.left) / rect.width; 185 frac = Math.max(0, Math.min(1, frac)); 186 // Centre the visible window on the pointer rather than putting its left edge 187 // there, so the strip tracks where you are pointing instead of lagging by a 188 // window's width. 189 var span = track.scrollWidth - track.clientWidth; 190 if (span <= 0) return; 191 var half = track.clientWidth / 2; 192 track.scrollLeft = Math.max(0, Math.min(span, frac * track.scrollWidth - half)); 193 } 194 195 bar.addEventListener("pointerdown", function (ev) { 196 dragging = true; 197 bar.setPointerCapture(ev.pointerId); 198 bar.classList.add("dragging"); 199 scrollToPointer(ev); 200 ev.preventDefault(); 201 }); 202 bar.addEventListener("pointermove", function (ev) { 203 if (dragging) scrollToPointer(ev); 204 }); 205 function endDrag(ev) { 206 if (!dragging) return; 207 dragging = false;
208 bar.classList.remove("dragging"); 209 try { bar.releasePointerCapture(ev.pointerId); } catch (e) { } 210 } 211 bar.addEventListener("pointerup", endDrag); 212 bar.addEventListener("pointercancel", endDrag); 213 } 214 215 track.addEventListener("scroll", sync, { passive: true }); 216 window.addEventListener("resize", sync); 217 sync(); 218 } 219 220 function boot() { 221 document.querySelectorAll(".fsum").forEach(init); 222 } 223 224 // Called by Blazor after it renders the summary. The stored public page has its markup 225 // at load and boots itself below; in the app the components render after the circuit 226 // connects, long after DOMContentLoaded - so nothing folded the drill-downs and every 227 // stage sat open. Safe to call as often as it likes: init binds once per root. 228 window.mfsfSummaryInit = boot; 229 230 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); 231 else boot(); 232})();
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.