1/** 2 * Pollsmax site-wide dark/light theme. 3 * Attribute: html[data-theme="dark"|"light"] 4 * Storage: localStorage["pollsmax-theme"] 5 */ 6(function () { 7 'use strict'; 8 9 if (window.PollsmaxTheme && window.PollsmaxTheme.__initialized) { 10 return; 11 } 12 13 var STORAGE_KEY = 'pollsmax-theme'; 14 var LEGACY_STORAGE_KEY = 'pollsmax-home-theme'; 15 var TOGGLE_ID = 'pollsmax-theme-toggle'; 16 var MEDIA_QUERY = '(prefers-color-scheme: light)'; 17 var userHasExplicitTheme = false; 18 19 function readSavedTheme() { 20 try { 21 var saved = localStorage.getItem(STORAGE_KEY); 22 if (saved === 'dark' || saved === 'light') return saved; 23 var legacy = localStorage.getItem(LEGACY_STORAGE_KEY); 24 if (legacy === 'dark' || legacy === 'light') { 25 try { localStorage.setItem(STORAGE_KEY, legacy); } catch (eMig) { /* ignore */ } 26 return legacy; 27 } 28 } catch (eRead) { /* ignore */ } 29 return null; 30 } 31 32 function getInitialTheme() { 33 return 'dark'; 34 } 35 36 function currentTheme() { 37 return document.documentElement.dataset.theme === 'light' ? 'light' : 'dark'; 38 } 39 40 function isLight() { 41 return currentTheme() === 'light'; 42 } 43 44 function cssVar(name, fallback) { 45 try { 46 var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); 47 if (v) return v; 48 } catch (eCss) { /* ignore */ } 49 return fallback; 50 } 51 52 function chartColors() { 53 return { 54 background: cssVar('--color-chart-bg', isLight() ? '#ffffff' : '#0c0f14'), 55 chartBg: cssVar('--color-chart-bg', isLight() ? '#ffffff' : '#0c0f14'), 56 grid: cssVar('--color-chart-grid', isLight() ? 'rgba(15, 23, 42, 0.09)' : 'rgba(31, 41, 55, 0.45)'), 57 border: cssVar('--color-chart-grid', isLight() ? 'rgba(15, 23, 42, 0.09)' : 'rgba(31, 41, 55, 0.45)'), 58 tick: cssVar('--color-chart-axis', isLight() ? '#596575' : '#94a3b8'), 59 axis: cssVar('--color-chart-axis', isLight() ? '#596575' : '#94a3b8'), 60 text: cssVar('--color-text-primary', isLight() ? '#10141b' : '#e2e8f0'), 61 secondaryText: cssVar('--color-text-secondary', isLight() ? '#46505f' : '#94a3b8'), 62 textSecondary: cssVar('--color-text-secondary', isLight() ? '#46505f' : '#94a3b8'), 63 tooltipBg: cssVar('--color-chart-tooltip-bg', isLight() ? '#ffffff' : '#161d28'), 64 tooltipBorder: cssVar('--color-chart-tooltip-border', isLight() ? 'rgba(15, 23, 42, 0.17)' : 'rgba(255, 255, 255, 0.14)') 65 }; 66 } 67 68 function getThemeChartColors() { 69 return chartColors(); 70 } 71 72 function mapNoElectionFill() { 73 return cssVar('--color-map-no-election', isLight() ? '#e2e8f0' : '#0f172a'); 74 } 75 76 function mapStateStroke() { 77 /* Dark mode must stay near-black (#1f2937) â never text gray. */ 78 return cssVar('--color-map-state-stroke', isLight() ? '#cbd5e1' : '#1f2937'); 79 } 80 81 function mapStateHoverStroke() { 82 /* Dedicated map token â do NOT reuse text-secondary (#94a3b8 is light gray). */ 83 return cssVar('--color-map-hover-stroke', isLight() ? '#334155' : '#1f2937'); 84 } 85 86 function mapFlipOutlineStroke() { 87 return cssVar('--color-map-flip-outline', isLight() ? '#000000' : '#ffffff'); 88 } 89 90 function flipChipOutlineStyle() { 91 var stroke = mapFlipOutlineStroke(); 92 return 'box-shadow:inset 0 0 0 1px ' + stroke + ';'; 93 } 94 95 function themeToggleInnerHtml() { 96 return '<svg class="pollsmax-theme-toggle__moon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' + 97 '<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"/>' + 98 '</svg>' + 99 '<svg class="pollsmax-theme-toggle__sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' + 100 '<circle cx="12" cy="12" r="4"/>' + 101 '<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41
101M17.66 6.34l1.41-1.41"/>' + 102 '</svg>'; 103 } 104 105 function findThemeToggleBtn() { 106 return document.getElementById(TOGGLE_ID) || 107 document.getElementById('pollsmax-home-theme-toggle') || 108 document.querySelector('.theme-toggle, .pollsmax-theme-toggle'); 109 } 110 111 function updateThemeToggle(theme) { 112 var btn = findThemeToggleBtn(); 113 if (!btn) return; 114 var light = theme === 'light'; 115 var label = light ? 'Switch to dark mode' : 'Switch to light mode'; 116 btn.setAttribute('aria-pressed', light ? 'true' : 'false'); 117 btn.setAttribute('aria-label', label); 118 btn.title = label; 119 btn.classList.toggle('is-light', light); 120 } 121 122 function updateToggleUi() { 123 updateThemeToggle(currentTheme()); 124 } 125 126 function bindToggle(btn) { 127 if (!btn || btn.dataset.pmThemeBound === '1') return; 128 btn.dataset.pmThemeBound = '1'; 129 btn.addEventListener('click', function () { 130 toggleTheme(); 131 }); 132 } 133 134 function hideThemeToggles() { 135 var nodes = document.querySelectorAll( 136 '#pollsmax-theme-toggle, #pollsmax-home-theme-toggle, .pollsmax-theme-toggle, .theme-toggle' 137 ); 138 for (var i = 0; i < nodes.length; i++) { 139 nodes[i].hidden = true; 140 nodes[i].setAttribute('aria-hidden', 'true'); 141 nodes[i].style.setProperty('display', 'none', 'important'); 142 nodes[i].style.setProperty('visibility', 'hidden', 'important'); 143 nodes[i].style.setProperty('pointer-events', 'none', 'important'); 144 } 145 } 146 147 function injectToggle() { 148 hideThemeToggles(); 149 return null; 150 } 151 152 function refreshAllCharts() { 153 if (window.ET && typeof ET.refreshAllLineChartAxisThemes === 'function') { 154 ET.refreshAllLineChartAxisThemes(); 155 return; 156 } 157 if (!window.Chart || typeof Chart.getChart !== 'function') return; 158 var ax = chartColors(); 159 document.querySelectorAll('canvas').forEach(function (canvas) { 160 var ch = Chart.getChart(canvas); 161 if (!ch || !ch.options || !ch.options.scales) return; 162 if (window.ET && typeof ET.applyLineChartAxisGridTheme === 'function') { 163 ET.applyLineChartAxisGridTheme(ch); 164 return; 165 } 166 var scales = ch.options.scales; 167 ['x', 'y'].forEach(function (axisKey) { 168 var scale = scales[axisKey]; 169 if (!scale) return; 170 if (scale.ticks) scale.ticks.color = ax.tick; 171 if (scale.grid && typeof scale.grid.color === 'string') { 172 scale.grid.color = ax.grid; 173 } 174 if (scale.border && scale.border.color) scale.border.color = ax.border; 175 }); 176 var labels = (ch.data && ch.data.labels) ? ch.data.labels : []; 177 if (scales.x && scales.x.grid && window.ET && labels.length) { 178 if (typeof ET.senGovHubChartXGridColorFn === 'function') { 179 scales.x.grid.color = ET.senGovHubChartXGridColorFn(labels, ax.grid); 180 } else if (typeof ET.xGridColorMonthStarts === 'function') { 181 scales.x.grid.color = ET.xGridColorMonthStarts(labels, ax.grid); 182 } 183 } 184 try { ch.update('none'); } catch (eUp) { /* ignore */ } 185 }); 186 } 187 188 function refreshMapsAndBars() { 189 if (!window.ET) return; 190 var mapPairs = [ 191 ['usMap', 'mapTip'], 192 ['govMap', 'govMapTip'] 193 ]; 194 mapPairs.forEach(function (pair) { 195 if (!document.getElementById(pair[0])) return; 196 try { 197 if (pair[0] === 'usMap' && typeof ET.buildMap === 'function') ET.buildMap(pair[0], pair[1]); 198 if (pair[0] === 'govMap' && typeof ET.buildGovMap === 'function') ET.buildGovMap(pair[0], pair[1]); 199 } catch (eM) { /* ignore */ } 200 }); 201 try { 202 if (typeof ET.updateSeatBar === 'function' && document.getElementById('barR') && 203 !(typeof ET.isHouseHubSeatBarPage === 'function' && ET.isHouseHubSeatBarPage())) { 204 ET.updateSeatBar({ r: 'barR', d: 'barD', tie: 'barTie', nr: 'numR', nd: 'numD', rFlip: 'barRFlip', dFlip: 'barDFlip' }); 205 } 206 } catch (eB) { /* ignore */ } 207 try { 208 if (typeof ET.updateGovSeatBar === 'function' && document.getElementById('govBarR')) { 209 ET.updateGovSeatBar({ r: 'govBarR', d: 'govBarD', nr: 'govNumR', nd: 'govNumD', rFlip: 'govBarRFlip', dFlip: 'govBarDFlip' }); 210 } 211 } catch (eG) { /* ignore */ } 212 if (typeof ET.refreshElectoralHistoryMap === 'function') { 213 try { ET.refreshElectoralHistoryMap(); } catch (eEh) { /* ignore */ } 214 } 215 if (document.getElementById('houseCdMap') && typeof ET.refreshHouseHubFlipOutlineStrokes === 'function') { 216 try { ET.refreshHouseHubFlipOutlineStrokes('houseCdMap'); } catch (eH) { /* ignore */ } 217 } 218 } 219 220 function refreshHomeCharts() { 221 if (typeof window.pollsmaxInitGenericBallotHomeChart === 'function') { 222 window.pollsmaxInitGenericBallotHomeChart(); 223 } 224 if (typeof window.pollsmaxInitTrumpApprovalHomeChart === 'function') { 225 window.pollsmaxInitTrumpApprovalHomeChart(); 226 } 227 if (typeof window.pollsmaxInit2028PrimaryHomeCharts === 'function') { 228 window.pollsmaxInit2028PrimaryHomeCharts(); 229 } 230 if (typeof window.__pollsmaxHomeSearchPollsRerender === 'function') { 231 window.__pollsmaxHomeSearchPollsRerender(); 232 } 233 } 234 235 function refreshSiteVisuals() { 236 document.dispatchEvent(new CustomEvent('pollsmax-theme-change', {
237 detail: { theme: currentTheme(), light: isLight() } 238 })); 239 refreshMapsAndBars(); 240 refreshHomeCharts(); 241 refreshAllCharts(); 242 if (window.ET) { 243 if (typeof ET.refreshRaceForecastSimHistogramForTheme === 'function') { 244 try { ET.refreshRaceForecastSimHistogramForTheme(); } catch (eRfHist) { /* ignore */ } 245 } else if (typeof ET.refreshRaceForecastPageIfMounted === 'function') { 246 try { ET.refreshRaceForecastPageIfMounted({ force: true }); } catch (eRf) { /* ignore */ } 247 } 248 if (typeof ET.refreshHubChartsLayout === 'function') { 249 try { ET.refreshHubChartsLayout(); } catch (eHub) { /* ignore */ } 250 } 251 } 252 } 253 254 /* INP: flip theme attrs on the click task; rebuild maps/charts after next paint. */ 255 var themeRefreshTimer = 0; 256 function scheduleRefreshSiteVisuals() { 257 if (themeRefreshTimer) return; 258 var run = function () { 259 themeRefreshTimer = 0; 260 refreshSiteVisuals(); 261 }; 262 var afterPaint = function () { 263 if (typeof window.scheduler === 'object' && 264 window.scheduler && 265 typeof window.scheduler.yield === 'function') { 266 window.scheduler.yield().then(run).catch(run); 267 return; 268 } 269 themeRefreshTimer = setTimeout(run, 0); 270 }; 271 if (typeof requestAnimationFrame === 'function') { 272 themeRefreshTimer = -1; 273 requestAnimationFrame(function () { 274 themeRefreshTimer = 0; 275 afterPaint(); 276 }); 277 } else { 278 afterPaint(); 279 } 280 } 281 282 function applyTheme(theme, save, opts) { 283 var next = 'dark'; 284 var skipRefresh = !!(opts && opts.skipRefresh); 285 document.documentElement.setAttribute('data-theme', next); 286 document.documentElement.removeAttribute('data-pollsmax-theme'); 287 document.documentElement.style.colorScheme = next; 288 if (save) { 289 userHasExplicitTheme = true; 290 try { 291 localStorage.setItem(STORAGE_KEY, next); 292 localStorage.setItem(LEGACY_STORAGE_KEY, next); 293 } catch (eStore) { /* ignore */ } 294 } 295 updateThemeToggle(next); 296 if (!skipRefresh) scheduleRefreshSiteVisuals(); 297 } 298 299 function setTheme(mode) { 300 applyTheme('dark', true); 301 } 302 303 function toggleTheme() { 304 applyTheme('dark', true); 305 } 306 307 function enableThemeTransitions() { 308 if (document.documentElement.classList.contains('theme-transition-enabled')) return; 309 var enable = function () { 310 document.documentElement.classList.add('theme-transition-enabled'); 311 }; 312 if (typeof requestAnimationFrame === 'function') { 313 requestAnimationFrame(function () { 314 requestAnimationFrame(enable); 315 }); 316 } else { 317 setTimeout(enable, 50); 318 } 319 } 320 321 function bindSystemPreferenceListener() { 322 /* Site is locked to dark mode; ignore OS light/dark. */ 323 } 324 325 function init() { 326 if (window.PollsmaxTheme && window.PollsmaxTheme.__initialized) return; 327 applyTheme('dark', true, { skipRefresh: true }); 328 hideThemeToggles(); 329 enableThemeTransitions(); 330 window.addEventListener('resize', hideThemeToggles); 331 if (window.PollsmaxTheme) { 332 window.PollsmaxTheme.__initialized = true; 333 } 334 } 335 336 var api = { 337 __initialized: false, 338 STORAGE_KEY: STORAGE_KEY, 339 isLight: isLight, 340 getTheme: currentTheme, 341 getInitialTheme: getInitialTheme, 342 chartColors: chartColors, 343 getThemeChartColors: getThemeChartColors, 344 mapNoElectionFill: mapNoElectionFill, 345 mapStateStroke: mapStateStroke, 346 mapStateHoverStroke: mapStateHoverStroke, 347 mapFlipOutlineStroke: mapFlipOutlineStroke, 348 flipChipOutlineStyle: flipChipOutlineStyle, 349 applyTheme: applyTheme, 350 setTheme: setTheme, 351 toggleTheme: toggleTheme, 352 injectToggle: injectToggle, 353 updateThemeToggle: updateThemeToggle, 354 refreshSiteVisuals: refreshSiteVisuals 355 }; 356 357 window.PollsmaxTheme = api; 358 window.PollsmaxHomeTheme = api; 359 window.getThemeChartColors = getThemeChartColors; 360 361 if (document.readyState === 'loading') { 362 document.addEventListener('DOMContentLoaded', init); 363 } else { 364 init(); 365 } 366})();
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.