PageSourceSearch

https://app.myfs.flights/js/flightsummary.js?v=SeZnqf18ZKhCiPiHAr8ue9MTOriOe17YX5kLXhCIDXQ

js myfs.flights collected 2026-09-25 14:41:45 UTC 11,004 bytes, 232 lines download raw bytes

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.