1/** 2 * Press Box MLB â Front-end JavaScript 3 * 4 * Vanilla JS IIFE. Key behaviors: 5 * - UTC time conversion: [data-utc] elements â visitor's local timezone 6 * - Tab switching: standings + roster with localStorage persistence 7 * - Sortable table headers on roster 8 */ 9(function () { 10 'use strict'; 11 12 /** 13 * Convert UTC timestamps to local timezone. 14 */ 15 function convertUtcTimes() { 16 var elements = document.querySelectorAll('[data-utc]'); 17 elements.forEach(function (el) { 18 var utc = el.getAttribute('data-utc'); 19 if (!utc) return; 20 21 var date = new Date(utc); 22 if (isNaN(date.getTime())) return; 23 24 var format = el.getAttribute('data-format') || 'time'; 25 var options; 26 27 if (format === 'date') { 28 options = { month: 'short', day: 'numeric' }; 29 } else if (format === 'datetime') { 30 options = { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }; 31 } else { 32 options = { hour: 'numeric', minute: '2-digit' }; 33 } 34 35 el.textContent = date.toLocaleString(undefined, options); 36 el.removeAttribute('data-utc'); 37 }); 38 } 39 40 /** 41 * Fetch a Team Tag panel's rendered HTML from REST and inject it. 42 * Runs once per panel; subsequent tab clicks are no-ops. 43 */ 44 function loadLazyPanel(panel) { 45 var api = window.pbMlbApi; 46 if (!api || !api.root) return; 47 48 var slug = panel.getAttribute('data-pb-mlb-lazy'); 49 var team = panel.getAttribute('data-pb-mlb-team'); 50 if (!slug || !team) return; 51 52 // Prevent duplicate fetches (handles rapid tab-click toggling). 53 panel.removeAttribute('data-pb-mlb-lazy'); 54 55 var url = api.root + 'team-tag/panel?panel=' + encodeURIComponent(slug) + '&team=' + encodeURIComponent(team); 56 fetch(url, { credentials: 'same-origin' }) 57 .then(function (r) { return r.json(); }) 58 .then(function (res) { 59 if (res && res.success && res.data && res.data.html) { 60 panel.innerHTML = res.data.html; 61 convertUtcTimes(); 62 initTabs(); 63 initSortableTables(); 64 initHeadshotFallbacks(); 65 } else { 66 var msg = (res && res.error && res.error.message) ? res.error.message : 'Unable to load.'; 67 var safe = document.createElement('p'); 68 safe.className = 'pb-mlb-team-tag__lazy-error'; 69 safe.textContent = msg; 70 panel.innerHTML = ''; 71 panel.appendChild(safe); 72 } 73 }) 74 .catch(function () { 75 var safe = document.createElement('p'); 76 safe.className = 'pb-mlb-team-tag__lazy-error'; 77 safe.textContent = 'Unable to load.'; 78 panel.innerHTML = ''; 79 panel.appendChild(safe); 80 panel.setAttribute('data-pb-mlb-lazy', slug); 81 }); 82 } 83 84 /** 85 * Tab switching with localStorage persistence. 86 */ 87 function initTabs() { 88 var tabGroups = document.querySelectorAll('[data-pb-mlb-tabs]'); 89 tabGroups.forEach(function (group) { 90 if (group.dataset.pbTabsBound) return; 91 group.dataset.pbTabsBound = '1'; 92 var groupId = group.getAttribute('data-pb-mlb-tabs'); 93 var buttons = group.querySelectorAll('[data-pb-mlb-tab]'); 94 // Only match panels that are direct children of the same parent, 95 // not nested panels inside sub-templates (standings, roster tabs). 96 var parent = group.parentElement; 97 var panels = Array.prototype.filter.call( 98 parent.querySelectorAll('[data-pb-mlb-panel]'), 99 function (p) { return p.parentElement === parent; } 100 ); 101 102 // Restore saved tab â skip for team-tag (always opens on Today). 103 if (groupId !== 'team-tag') { 104 var saved = localStorage.getItem('pb_mlb_tab_' + groupId); 105 if (saved) { 106 activateTab(saved); 107 } 108 } 109 110 buttons.forEach(function (btn) { 111 btn.addEventListener('click', function () { 112 var tab = btn.getAttribute('data-pb-mlb-tab'); 113 activateTab(tab); 114 if (groupId !== 'team-tag') { 115 localStorage.setItem('pb_mlb_tab_' + groupId, tab); 116 } 117 }); 118 }); 119 120 function activateTab(tab) { 121 var activeBtn = null; 122 buttons.forEach(function (b) { 123 var isActive = b.getAttribute('data-pb-mlb-tab') === tab; 124 b.classList.toggle('pb-mlb-tab--active', isActive); 125 if (isActive) activeBtn = b; 126 }); 127 panels.forEach(function (p) { 128 var active = p.getAttribute('data-pb-mlb-panel') === tab; 129 p.style.display = active ? '' : 'none'; 130 if (active && p.hasAttribute('data-pb-mlb-lazy')) { 131 loadLazyPanel(p); 132 } 133 }); 134 // Roster tab filters rows within a single shared table rather 135 // than swapping panels, so adjust row visibility + column layout. 136 if (groupId === 'roster' && activeBtn) { 137 var table = parent.querySelector('.pb-mlb-roster__table'); 138 if (table) { 139 var layout = activeBtn.getAttribute('data-pb-mlb-roster-layout') || 'hitting'; 140 table.setAttribute('data-pb-mlb-roster-layout', layout); 141 var rows = table.querySelectorAll('.pb-mlb-roster__row'); 142 rows.forEach(function (row) { 143 row.hidden = (tab !== 'all') && (row.getAttribute('data-pb-mlb-position-group') !== tab); 144 }); 145 } 146 } 147 } 148 }); 149 } 150 151 /** 152 * Initialize sortable table headers. 153 * 154 * Headers with data-sort="key" become clickable. 155 * Cells use data-sort-{key}="value" attributes for sort values, 156 * falling back to textContent if missing. 157 * data-sort-type="text" on the th forces alphabetical sorting (default: numeric). 158 * data-sort-default-dir="desc" sets the initial direction for first click. 159 */ 160 function initSortableTables() { 161 var tables = document.querySelectorAll('.pb-mlb-roster__table'); 162
163 tables.forEach(function (table) { 164 if (table.dataset.pbSortBound) return; 165 table.dataset.pbSortBound = '1'; 166 167 var headers = table.querySelectorAll('th[data-sort]'); 168 if (!headers.length) return; 169 170 var thead = table.querySelector('thead'); 171 var tbody = table.querySelector('tbody'); 172 if (!thead || !tbody) return; 173 174 var allTh = thead.querySelectorAll('th'); 175 var colIndex = {}; 176 allTh.forEach(function (th, idx) { 177 var key = th.getAttribute('data-sort'); 178 if (key) colIndex[key] = idx; 179 }); 180 181 function isEmptyVal(v) { 182 var trimmed = (v || '').replace(/\s+/g, ' ').trim(); 183 return trimmed === '' || trimmed === '\u2014'; 184 } 185 186 function handleSort(th) { 187 var key = th.getAttribute('data-sort'); 188 var idx = colIndex[key]; 189 if (idx === undefined) return; 190 191 var currentDir = th.getAttribute('data-sort-dir'); 192 var defaultDir = th.getAttribute('data-sort-default-dir') || 'asc'; 193 var dir = currentDir ? (currentDir === 'asc' ? 'desc' : 'asc') : defaultDir; 194 195 headers.forEach(function (h) { 196 h.classList.remove('pb-mlb-sortable__th--sorted'); 197 h.removeAttribute('data-sort-dir'); 198 h.setAttribute('aria-sort', 'none'); 199 }); 200 201 th.classList.add('pb-mlb-sortable__th--sorted'); 202 th.setAttribute('data-sort-dir', dir); 203 th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending'); 204 205 var attrName = 'data-sort-' + key; 206 var isText = th.getAttribute('data-sort-type') === 'text'; 207 208 // Pre-read sort values once per row to avoid repeated DOM lookups. 209 var entries = Array.prototype.map.call(tbody.querySelectorAll('tr'), function (row) { 210 var cell = row.children[idx]; 211 var val = cell ? (cell.getAttribute(attrName) || cell.textContent || '') : ''; 212 return { row: row, val: val }; 213 }); 214
215 entries.sort(function (a, b) { 216 var aEmpty = isEmptyVal(a.val); 217 var bEmpty = isEmptyVal(b.val); 218 if (aEmpty && bEmpty) return 0; 219 if (aEmpty) return 1; 220 if (bEmpty) return -1; 221 222 var cmp; 223 if (isText) { 224 cmp = a.val.localeCompare(b.val); 225 } else { 226 cmp = parseFloat(a.val) - parseFloat(b.val); 227 } 228 return dir === 'asc' ? cmp : -cmp; 229 }); 230 231 entries.forEach(function (e) { tbody.appendChild(e.row); }); 232 } 233 234 headers.forEach(function (th) { 235 th.setAttribute('role', 'button'); 236 th.setAttribute('tabindex', '0'); 237 th.setAttribute('aria-sort', 'none'); 238 239 th.addEventListener('click', function () { handleSort(th); }); 240 th.addEventListener('keydown', function (e) { 241 if (e.key === 'Enter' || e.key === ' ') { 242 e.preventDefault(); 243 handleSort(th); 244 } 245 }); 246 }); 247 }); 248 } 249 250 /** 251 * Replace broken headshot images with the MLB logo silhouette. 252 */ 253 function initHeadshotFallbacks() { 254 function applyFallback(img) { 255 img.onerror = null; 256 img.style.display = 'none'; 257 // Insert a CSS circle placeholder if one doesn't already exist. 258 if (!img.nextElementSibling || !img.nextElementSibling.classList.contains('pb-mlb-headshot-fallback')) { 259 var circle = document.createElement('span'); 260 circle.className = 'pb-mlb-headshot-fallback'; 261 circle.style.display = 'inline-block'; 262 circle.style.width = (img.width || 48) + 'px'; 263 circle.style.height = (img.height || 48) + 'px'; 264 circle.style.borderRadius = '50%'; 265 circle.style.background = '#002D72'; 266 circle.style.flexShrink = '0'; 267 img.parentNode.insertBefore(circle, img.nextSibling); 268 } 269 } 270 271 var imgs = document.querySelectorAll('img[src*="mlb-photos"][src*="headshot"]'); 272 imgs.forEach(function (img) { 273 if (img.dataset.pbFallbackBound) return; 274 img.dataset.pbFallbackBound = '1'; 275 img.addEventListener('error', function () { applyFallback(img); }); 276 if (img.complete && img.naturalWidth === 0) { 277 applyFallback(img); 278 } 279 }); 280 } 281 282 /** 283 * Initialize on DOM ready. 284 */ 285 function init() { 286 convertUtcTimes(); 287 initTabs(); 288 initSortableTables(); 289 initHeadshotFallbacks(); 290 } 291 292 if (document.readyState === 'loading') { 293 document.addEventListener('DOMContentLoaded', init); 294 } else { 295 init(); 296 } 297})();
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.