PageSourceSearch

https://www.pollsmax.com/assets/forecast-hub-mobile-races.js?v=hub-vote-fs-shrink-1

js pollsmax.com collected 2026-09-28 06:38:02 UTC 21,100 bytes, 496 lines download raw bytes

1/**
2 * Mobile hub pages: All Races list after simulation charts, above Search Polls.
3 */
4(function () {
5    'use strict';
6    var ET = window.ET = window.ET || {};
7
8    var VISIBLE_ROWS = 10;
9    var MOBILE_SCROLL_BOTTOM_TRIM_PX = 2;
10    var _cfg = null;
11    var _resizeTimer = null;
12
13    function isHubMobileRacesViewport() {
14        if (typeof ET.isMobileLayout === 'function') return ET.isMobileLayout();
15        return window.matchMedia('(max-width: 767px)').matches;
16    }
17
18    function detectConfig() {
19        if (document.body.classList.contains('senate-hub-page')) {
20            return {
21                kind: 'senate',
22                svgId: 'usMap',
23                mapSourceRowId: 'senateHubMapSourceRow',
24                desktopRowsId: 'allSenateRaceRows'
25            };
26        }
27        if (document.body.classList.contains('gov-hub-page')) {
28            return {
29                kind: 'gov',
30                svgId: 'govMap',
31                mapSourceRowId: 'govHubMapSourceRow',
32                desktopRowsId: 'allSenateRaceRows'
33            };
34        }
35        if (document.body.classList.contains('house-hub-page')) {
36            return {
37                kind: 'house',
38                svgId: 'houseCdMap',
39                mapSourceRowId: 'houseHubMapSourceRow',
40                desktopRowsId: 'allHouseRaceRows'
41            };
42        }
43        return null;
44    }
45
46    function desktopAside() {
47        if (!_cfg) return null;
48        if (_cfg.kind === 'gov') {
49            return document.querySelector('main .gov-hub-aside.senate-races-aside');
50        }
51        return document.querySelector('main .senate-races-aside:not(.gov-hub-aside)');
52    }
53
54    function mobileSection() {
55        return document.getElementById('hubMobileRacesSection');
56    }
57
58    function racesTitle() {
59        var aside = desktopAside();
60        var h2 = aside ? aside.querySelector('.hub-aside-head-rule h2, .forecast-hub-aside-head-row h2') : null;
61        if (h2 && h2.textContent) return h2.textContent.trim();
62        if (_cfg.kind === 'house') return 'All House Races';
63        if (_cfg.kind === 'gov') return 'All Governor Races';
64        return 'All Senate Races';
65    }
66
67    function stateSortLabel() {
68        var btn = document.getElementById('sortStateBtn');
69        if (!btn) return _cfg.kind === 'house' ? 'DISTRICT' : 'STATE';
70        var txt = btn.textContent.replace(/[↓↑]/g, '').trim();
71        return txt || (_cfg.kind === 'house' ? 'DISTRICT' : 'STATE');
72    }
73
74    function pollingSortLabel() {
75        var btn = document.getElementById('sortPollingBtn');
76        if (!btn) return 'POLLING';
77        return btn.textContent.replace(/[↓↑]/g, '').trim() || 'POLLING';
78    }
79
80    function marginSortLabel() {
81        var btn = document.getElementById('sortMarginBtn');
82        if (!btn) return 'MARGIN';
83        return btn.textContent.replace(/[↓↑]/g, '').trim() || 'MARGIN';
84    }
85
86    function sortArrowHtml(desktopId, mobileId) {
87        var desktop = document.getElementById(desktopId);
88        var arrow = desktop ? desktop.querySelector('.senate-sort-arrow') : null;
89        var dir = arrow ? arrow.textContent : '↓';
90        return '<span id="' + mobileId + '" class="senate-sort-arrow">' + dir + '</span>';
91    }
92
93    function buildSortRowHtml() {
94        return '' +
95            '<button id="hubMobileSortStateBtn" class="senate-sort-btn senate-sort-state shrink-0" type="button">' +
96            stateSortLabel() + ' ' + sortArrowHtml('sortStateBtn', 'hubMobileSortStateArrow') +
97            '</button>' +
98            '<div class="senate-races-sort-metrics">' +
99            '<div class="senate-sort-cands-col">' +
100            '<button id="hubMobileSortPollingBtn" class="senate-sort-btn senate-sort-polling" type="button">' +
101            pollingSortLabel() + ' ' + sortArrowHtml('sortPollingBtn', 'hubMobileSortPollingArrow') +
102            '</button></div>' +
103            '<button id="hubMobileSortMarginBtn" class="senate-sort-btn senate-sort-margin" type="button">' +
104            marginSortLabel() + ' ' + sortArrowHtml('sortMarginBtn', 'hubMobileSortMarginArrow') +
105            '</button></div>';
106    }
107
108    function buildHouseFilterToolbarHtml() {
109        if (typeof ET.houseHubRacesFilterToolbarHtml === 'function') {
110            return ET.houseHubRacesFilterToolbarHtml();
111        }
112        return '';
113    }
114
115    function buildMapSourceToggleHtml() {
116        var isHouse = _cfg && _cfg.kind === 'house';
117        var forecastTip = isHouse
118            ? 'Projection based on Pollsmax\'s proprietary election forecasting model.'
119            : 'Projections from our proprietary forecasting model, utilizing 100,000 simulations based on statewide and national polling data, race ratings, previous election results, and other factors.';
120        var pollingHtml = isHouse
121            ? '<span class="forecast-hub-map-source__btn forecast-hub-map-source__btn--polling-disabled" data-source-tip="Pollsmax does not have enough polling data yet to create a polling-based projection." aria-disabled="true" tabindex="0">Polling</span>'
122            : '<button type="button" class="forecast-hub-map-source__btn" data-map-source="polling" data-source-tip="Projections based solely on our time-weighted and partisanship-adjusted polling averages.">Polling</button>';
123        return '' +
124            '<div class="forecast-hub-map-source-head forecast-hub-map-source-row hub-mobile-races-map-source-cell" data-hub-mobile-map-source="1">' +
125            '<div class="forecast-hub-map-source race-view-toggle" role="group" aria-label="Race list data source">' +
126            '<button type="button" class="forecast-hub-map-source__btn" data-map-source="ratings" data-source-tip="' + forecastTip + '">Forecast</button>' +
127            pollingHtml +
128            '</div></div>';
129    }
130
131    function buildSectionHtml() {
132        var houseFilter = (_cfg && _cfg.kind === 'house') ? buildHouseFilterToolbarHtml() : '';
133        return '' +
134            '<section id="hubMobileRacesSection" class="hub-mobile-races-section senate-races-aside" aria-labelledby="hubMobileRacesHeading">' +
135            '<div class="hub-mobile-races-inner">' +
136            '<div class="hub-mobile-races-head-area forecast-hub-map-head-area">' +
137            '<div class="block border-b border-slate-800/80 pb-4 mb-3 hub-mobile-races-title-block">' +
138            '<h2 id="hubMobileRacesHeading" class="forecast-hub-map-title text-xl sm:text-2xl font-bold tracking-tight text-white" style="position:relative;top:2px;">' +
139            racesTitle() + '</h2></div>' +
140            '<div class="forecast-hub-map-head-slot hub-mobile-races-head-slot">' +
141            buildMapSourceToggleHtml() +
142            '</div></div>' +
143            '<div class="forecast-hub-aside-races-body hub-mobile-races-body">' +
144            houseFilter +
145            '<div class="mb-2 senate-races-sort-row hub-mobile-races-sort-row px-[6px]">' + buildSortRowHtml() + '</div>' +
146            '<div class="hub-mobile-races-scroll-wrap">' +
147            '<div id="hubMobileRaceRows" class="hub-mobile-races-rows"></div>' +
148            '</div>' +
149            '</div></div></section>';
150    }
151
152    function findSearchSection() {
153        return document.querySelector('.senate-right-content .senate-search-polls-section') ||
154            document.querySelector('.senate-search-polls-section');
155    }
156
157    function findSearchInsertPoint() {
158        var search = findSearchSection();
159        if (!search || !search.parentNode) return null;
160        return {
161            parent: search.parentNode,
162            before: search
163        };
164    }
165
166    function mountSectionAboveSearch(section) {
167        var insert = findSearchInsertPoint();
168        if (!insert || !section) return false;
169        if (section.parentNode !== insert.parent) {
170            insert.parent.insertBefore(section, insert.before);
171            return true;
172        }
173        if (insert.before && section.nextElementSibling !== insert.before) {
174            insert.parent.insertBefore(section, insert.before);
175            return true;
176        }
177        return true;
178    }
179
180    ET.repositionHubMobileRacesSection = function () {
181        if (!isHubMobileRacesViewport()) return;
182        var section = mobileSection();
183        if (!section) return;
184        mountSectionAboveSearch(section);
185    };
186
187    function ensureSection(cfg) {
188        _cfg = cfg;
189        if (!isHubMobileRacesViewport()) {
190            var existing = mobileSection();
191            if (existing) existing.hidden = true;
192            return null;
193        }
194        var insert = findSearchInsertPoint();
195        if (!insert || !insert.parent) return null;
196        var section = mobileSection();
197        if (!section) {
198            var wrap = document.createElement('div');
199            wrap.innerHTML = buildSectionHtml();
200            section = wrap.firstElementChild;
201            if (!section) return null;
202            insert.parent.insertBefore(section, insert.before);
203            bindMobileControls(cfg);
204        } else {
205            /* Static HTML shell: bind controls once, keep reserved height. */
206            if (!section.getAttribute('data-hub-mobile-races-bound')) {
207                section.setAttribute('data-hub-mobile-races-bound', '1');
208                bindMobileControls(cfg);
209            }
210            mountSectionAboveSearch(section);
211        }
212        section.hidden = false;
213        return section;
214    }
215
216    function bindMobileControls(cfg) {
217        bindOnce('hub-mobile-map-source', function () {
218            var mobileRow = document.querySelector('[data-hub-mobile-map-source="1"]');
219            if (!mobileRow || mobileRow.getAttribute('data-hub-mobile-map-bound')) return;
220            mobileRow.setAttribute('data-hub-mobile-map-bound', '1');
221            mobileRow.querySelectorAll('[data-map-source]').forEach(function (btn) {
222                btn.addEventListener('click', function () {
223                    var src = btn.getAttribute('data-map-source');
224                    var mapRow = document.getElementById(cfg.mapSourceRowId);
225                    if (!mapRow) return;
226                    var mapBtn = mapRow.querySelector('[data-map-source="' + src + '"]');
227                    if (mapBtn) mapBtn.click();
228                });
229            });
230            if (typeof ET.initForecastHubSourceTips === 'function') {
231                ET.initForecastHubSourceTips(mobileRow);
232            }
233        });
234
235        bindOnce('hub-mobile-sort-delegate', function () {
236            var body = document.querySelector('.hub-mobile-races-body');
237            if (!body) return;
238            body.addEventListener('click', function (ev) {
239                var t = ev.target;
240                if (!t || !t.closest) return;
241                var btn = t.closest('#hubMobileSortStateBtn, #hubMobileSortPollingBtn, #hubMobileSortMarginBtn');
242                if (!btn) return;
243                var mapId = btn.id === 'hubMobileSortStateBtn' ? 'sortStateBtn'
244                    : btn.id === 'hubMobileSortPollingBtn' ? 'sortPollingBtn'
245                        : 'sortMarginBtn';
246                var desktop = document.getElementById(mapId);
247                if (desktop) desktop.click();
248                requestAnimationFrame(function () {
249                    ET.syncHubMobileRacesFromDesktop(cfg.svgId);
250                });
251            });
252        });
253    }
254
255    function bindOnce(key, fn) {
256        var attr = 'data-hub-mobile-races-' + key;
257        if (document.body.getAttribute(attr)) return;
258        document.body.setAttribute(attr, '1');
259        fn();
260    }
261
262    function syncMobileMapSource(mapSource) {
263        var mobileRow = document.querySelector('[data-hub-mobile-map-source="1"]');
264        if (!mobileRow) return;
265        mobileRow.querySelectorAll('[data-map-source]').forEach(function (btn) {
266            var active = btn.getAttribute('data-map-source') === mapSource;
267            btn.classList.toggle('forecast-hub-map-source__btn--active', active);
268            btn.setAttribute('aria-pressed', active ? 'true' : 'false');
269        });
270    }
271
272    ET.syncHubMobileRacesMapSource = function (rowId, mapSource) {
273        if (!isHubMobileRacesViewport() || !mobileSection()) return;
274        if (_cfg && rowId && rowId !== _cfg.mapSourceRowId) return;
275        syncMobileMapSource(mapSource);
276    };
277
278    function syncMobileSortHeaders() {
279        var sortRow = document.querySelector('.hub-mobile-races-sort-row');
280        if (!sortRow) return;
281        sortRow.innerHTML = buildSortRowHtml();
282        bindMobileControls(_cfg);
283    }
284
285    function syncMobileVoteColVisibility() {
286        var section = mobileSection();
287        if (!section || !_cfg) return;
288        var threshold = 280;
289        if (_cfg.kind === 'house') {
290            threshold = (typeof ET.houseHubAsideVoteColHideRowWidth === 'number')
291                ? ET.houseHubAsideVoteColHideRowWidth
292                : ((typeof ET.hubAsideVoteColHideRowWidth === 'number') ? ET.hubAsideVoteColHideRowWidth : 280);
293        } else if (typeof ET.hubAsideVoteColHideRowWidth === 'number') {
294            threshold = ET.hubAsideVoteColHideRowWidth;
295        }
296        var sample = section.querySelector('#hubMobileRaceRows .senate-race-row');
297        var rowW = sample ? sample.getBoundingClientRect().width : section.getBoundingClientRect().width;
298        if (rowW > 0) {
299            section.dataset.hubAsideLastRowWidth = String(Math.round(rowW));
300        }
301        section.classList.toggle('hub-aside-hide-vote-col', rowW > 0 && rowW < threshold);
302        if (typeof ET.syncHubAsideVoteColRowFontSize === 'function') {
303            ET.syncHubAsideVoteColRowFontSize(section);
304        }
305        if (_cfg.kind === 'house') {
306            if (typeof ET.syncHouseHubAsideRaceRowLabels === 'function') {
307                ET.syncHouseHubAsideRaceRowLabels();
308            }
309        } else if (typeof ET.syncHubAsideStateRaceRowLabelsForAside === 'function') {
310            ET.syncHubAsideStateRaceRowLabelsForAside(section);
311        }
312        if (typeof ET.syncHubAsideProjectedVoteSortLabel === 'function') {
313            ET.syncHubAsideProjectedVoteSortLabel(_cfg.svgId);
314        }
315        syncMobileSortHeaders();
316    }
317
318    function syncMobileScrollHeight() {
319        var wrap = document.querySelector('.hub-mobile-races-scroll-wrap');
320        var rowsEl = document.getElementById('hubMobileRaceRows');
321        if (!wrap || !rowsEl) return;
322        var rows = rowsEl.querySelectorAll('.senate-race-row');
323        if (!rows.length) {
324            wrap.style.height = '';
325            wrap.style.maxHeight = '';
326            rowsEl.style.maxHeight = '';
327            return;
328        }
329        var count = Math.min(VISIBLE_ROWS, rows.length);
330        var first = rows[0];
331        var last = rows[count - 1];
332        var r0 = first.getBoundingClientRect();
333        var rLast = last.getBoundingClientRect();
334        var height = Math.ceil(rLast.bottom - r0.top);
335        if (height <= 0) {
336            height = Math.ceil(first.getBoundingClientRect().height * count);
337        }
338        height = Math.max(0, height - MOBILE_SCROLL_BOTTOM_TRIM_PX);
339        wrap.style.height = height + 'px';
340        wrap.style.maxHeight = height + 'px';
341        rowsEl.style.maxHeight = height + 'px';
342        wrap.classList.add('hub-mobile-races-scroll-wrap--ready');
343    }
344
345    ET.syncHubMobileRacesFromDesktop = function (svgId) {
346        if (!isHubMobileRacesViewport()) {
347            var hidden = mobileSection();
348            if (hidden) hidden.hidden = true;
349            return;
350        }
351        var cfg = detectConfig();
352        if (!cfg) return;
353        if (svgId && svgId !== cfg.svgId) return;
354        var section = ensureSection(cfg);
355        if (!section) return;
356
357        /*
358         * House label sync (syncMobileVoteColVisibility -> syncHouseHubAsideRaceRowLabels)
359         * writes textContent back into the OBSERVED desktop list. Left connected, the
360         * observer re-fires this sync in a ~16ms loop: #hubMobileRaceRows is re-cloned
361         * every tick (flag <img>s reset -> flash then blank) and the constant DOM churn
362         * detaches tapped rows before their click fires (map/list/search stop navigating).
363         * Detach while we mutate, then drop the self-caused records before re-observing.
364         */
365        var obs = _desktopRowsObs;
366        if (obs) obs.disconnect();
367
368        var desktopRows = document.getElementById(cfg.desktopRowsId);
369        var mobileRows = document.getElementById('hubMobileRaceRows');
370        if (desktopRows && mobileRows) {
371            mobileRows.innerHTML = desktopRows.innerHTML;
372        }
373
374        syncMobileSortHeaders();
375
376        var ui = typeof ET.getForecastHubMapUi === 'function' ? ET.getForecastHubMapUi(cfg.svgId) : null;
377        if (ui && ui.mapSource) syncMobileMapSource(ui.mapSource);
378
379        syncMobileVoteColVisibility();
380
381        var heading = document.getElementById('hubMobileRacesHeading');
382        if (heading) heading.textContent = racesTitle();
383
384        if (obs && _desktopRowsObsTarget) {
385            obs.takeRecords();
386            obs.observe(_desktopRowsObsTarget, { childList: true, subtree: true, characterData: true });
387        }
388
389        if (_cfg && _cfg.kind === 'house' && typeof ET.bindHouseHubRacesFilter === 'function') {
390            ET.bindHouseHubRacesFilter();
391        }
392
393        requestAnimationFrame(function () {
394            requestAnimationFrame(function () {
395                syncMobileScrollHeight();
396            });
397        });
398    };
399
400    var _desktopRowsObs = null;
401    var _desktopRowsObsTarget = null;
402    var _desktopRowsObsRetry = 0;
403
404    ET.initHubMobileRaces = function () {
405        if (!isHubMobileRacesViewport()) return;
406        var cfg = detectConfig();
407        if (!cfg) return;
408        ensureSection(cfg);
409        ET.syncHubMobileRacesFromDesktop(cfg.svgId);
410        bindDesktopRowsObserver(cfg);
411    };
412
413    ET.scheduleHubMobileRacesInit = function () {
414        /* House has ~435 rows — fewer retries; MutationObserver covers late desktop fills. */
415        var delays = (document.body && document.body.classList.contains('house-hub-page'))
416            ? [0, 100, 500]
417            : [0, 50, 200, 600, 1500, 3500];
418        delays.forEach(function (ms) {
419            setTimeout(function () {
420                ET.initHubMobileRaces();
421            }, ms);
422        });
423    };
424
425    function bindDesktopRowsObserver(cfg) {
426        if (_desktopRowsObs) return;
427        var desktopRows = document.getElementById(cfg.desktopRowsId);
428        if (!desktopRows) {
429            if (_desktopRowsObsRetry++ < 40) {
430                setTimeout(function () { bindDesktopRowsObserver(cfg); }, 100);
431            }
432            return;
433        }
434        if (!('MutationObserver' in window)) return;
435        var obsTimer;
436        _desktopRowsObs = new MutationObserver(function () {
437            if (ET._houseHubRacesFilterApplying) return;
438            clearTimeout(obsTimer);
439            obsTimer = setTimeout(function () {
440                if (ET._houseHubRacesFilterApplying) return;
441                ET.syncHubMobileRacesFromDesktop(cfg.svgId);
442            }, 16);
443        });
444        _desktopRowsObsTarget = desktopRows;
445        _desktopRowsObs.observe(desktopRows, { childList: true, subtree: true, characterData: true });
446    }
447
448    (function wrapAsideRefresh() {
449        var orig = ET.refreshForecastHubAsideRaces;
450        ET.refreshForecastHubAsideRaces = function (svgId) {
451            if (typeof orig === 'function') orig(svgId);
452            ET.syncHubMobileRacesFromDesktop(svgId);
453        };
454    })();
455
456    function scheduleResizeSync() {
457        if (document.body && document.body.classList.contains('house-hub-page') &&
458            typeof ET.isHouseHubMobile === 'function' && ET.isHouseHubMobile() &&
459            typeof ET.houseHubMobileWidthChanged === 'function' && !ET.houseHubMobileWidthChanged()) {
460            return;
461        }
462        clearTimeout(_resizeTimer);
463        _resizeTimer = setTimeout(function () {
464            if (!isHubMobileRacesViewport()) {
465                var section = mobileSection();
466                if (section) section.hidden = true;
467                return;
468            }
469            ET.initHubMobileRaces();
470        }, 80);
471    }
472
473    if (!window._hubMobileRacesResizeBound) {
474        window._hubMobileRacesResizeBound = true;
475        window.addEventListener('resize', scheduleResizeSync, { passive: true });
476        if (typeof window.matchMedia === 'function') {
477            var mobileMq = window.matchMedia('(max-width: 767px)');
478            if (typeof mobileMq.addEventListener === 'function') {
479                mobileMq.addEventListener('change', scheduleResizeSync);
480            } else if (typeof mobileMq.addListener === 'function') {
481                mobileMq.addListener(scheduleResizeSync);
482            }
483        }
484    }
485
486    function bootHubMobileRaces() {
487        ET.scheduleHubMobileRacesInit();
488    }
489
490    if (document.readyState === 'loading') {
491        document.addEventListener('DOMContentLoaded', bootHubMobileRaces);
492    } else {
493        bootHubMobileRaces();
494    }
495    window.addEventListener('load', bootHubMobileRaces);
496})();

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.