1/** 2 * Press Box â Front-end JavaScript. 3 * 4 * - Converts UTC game times to the visitor's local timezone. 5 * - Auto-refreshes live game data every 30 seconds and updates the DOM. 6 * - Pauses polling when the browser tab is hidden. 7 */ 8 9/* global wpApiSettings */ 10 11( function () { 12 'use strict'; 13 14 const REFRESH_INTERVAL = 30000; // 30 seconds for live games. 15 let refreshTimer = null; 16 17 /** 18 * Resolve a REST endpoint URL localized by pb_enqueue_assets(). 19 * 20 * Falls back to the legacy wpApiSettings/'/wp-json/' chain only if the 21 * localized data is missing. That fallback breaks on subdirectory installs 22 * and plain permalinks, so it exists purely as a last resort. 23 * 24 * @param {string} name Endpoint key from pbSettings.endpoints. 25 * @param {string} path REST path used to build the fallback URL. 26 * @return {string} Endpoint URL. 27 */ 28 function restEndpoint( name, path ) { 29 const endpoints = ( window.pbSettings && window.pbSettings.endpoints ) || {}; 30 if ( endpoints[ name ] ) { 31 return endpoints[ name ]; 32 } 33 const base = ( typeof wpApiSettings !== 'undefined' && wpApiSettings.root ) 34 ? wpApiSettings.root 35 : '/wp-json/'; 36 return base + path; 37 } 38 39 /** 40 * Append query params to an endpoint URL. 41 * 42 * Uses the URL API so params are appended with the correct separator â 43 * the plain-permalink endpoint form already carries a query string 44 * (?rest_route=/press-box/v1/...), where a naive '?' would corrupt it. 45 * 46 * @param {string} url Endpoint URL. 47 * @param {Object} params Query parameters. 48 * @return {string} URL with params applied. 49 */ 50 function withParams( url, params ) { 51 const parsed = new URL( url, window.location.origin ); 52 for ( const key of Object.keys( params ) ) { 53 parsed.searchParams.set( key, params[ key ] ); 54 } 55 return parsed.toString(); 56 } 57 58 /** 59 * Convert all [data-utc] time elements to the visitor's local time. 60 */ 61 function convertToLocalTimes( root ) { 62 const scope = root || document; 63 const elements = scope.querySelectorAll( '[data-utc]' ); 64 65 for ( const el of elements ) { 66 const utc = el.getAttribute( 'data-utc' ); 67 if ( ! utc ) { 68 continue; 69 } 70 71 try { 72 const date = new Date( utc ); 73 if ( isNaN( date.getTime() ) ) { 74 continue; 75 } 76 77 const formatted = new Intl.DateTimeFormat( undefined, { 78 hour: 'numeric', 79 minute: '2-digit', 80 } ).format( date ); 81 82 el.textContent = formatted; 83 } catch ( e ) { 84 // Keep server-rendered fallback on error. 85 } 86 } 87 } 88 89 /** 90 * Check if any game cards or schedule hero on the page are live. 91 * 92 * @return {boolean} True if live games are detected. 93 */ 94 function hasLiveGames() { 95 if ( document.querySelectorAll( '.pb-game--live, .pb-schedule__hero--live' ).length > 0 ) { 96 return true; 97 } 98 99 // A schedule hero showing "upcoming" may be stale â if the game's 100 // start time has passed, start polling so we can transition it to live. 101 const hero = document.querySelector( '.pb-schedule__hero[data-game-id]' ); 102 if ( hero ) { 103 const timeEl = hero.querySelector( '[data-utc]' ); 104 if ( timeEl ) { 105 const utc = timeEl.getAttribute( 'data-utc' ); 106 const start = new Date( utc ); 107 if ( ! isNaN( start.getTime() ) && start.getTime() <= Date.now() ) { 108 return true; 109 } 110 } 111 } 112 113 return false; 114 } 115 116 /** 117 * Format a period number as ordinal (1st, 2nd, 3rd, OT). 118 * 119 * @param {number} period Period number. 120 * @return {string} Ordinal string. 121 */ 122 function formatPeriod( period ) { 123 switch ( period ) { 124 case 1: 125 return '1st'; 126 case 2: 127 return '2nd'; 128 case 3: 129 return '3rd'; 130 default: 131 return 'OT'; 132 } 133 } 134 135 /** 136 * Update a game card with fresh data (scores, status, team positions). 137 * 138 * @param {Element} card Game card DOM element. 139 * @param {Object} game Normalized game data from the REST API. 140 */ 141 function updateGameCard( card, game ) { 142 const center = card.querySelector( '.pb-game__center' ); 143 if ( ! center ) { 144 return; 145 } 146
147 const isLive = ( game.state === 'LIVE' || game.state === 'CRIT' ); 148 const isFinal = ( game.state === 'FINAL' || game.state === 'OFF' ); 149 150 // Leading/winning team goes left; tied or scheduled keeps away/home order. 151 let left = game.away; 152 let right = game.home; 153 if ( ( isLive || isFinal ) && game.home.score > game.away.score ) { 154 left = game.home; 155 right = game.away; 156 } 157 158 // Update live/final border state. 159 card.classList.toggle( 'pb-game--live', isLive ); 160 161 // Update team sides. 162 const leftEl = card.querySelector( '.pb-game__team--left' ); 163 const rightEl = card.querySelector( '.pb-game__team--right' ); 164 if ( leftEl ) { 165 updateTeamEl( leftEl, left ); 166 } 167 if ( rightEl ) { 168 updateTeamEl( rightEl, right ); 169 } 170 171 if ( isLive || isFinal ) { 172 const scoreHtml = `<div class="pb-game__score">${ escapeHtml( String( left.score ) ) }\u2013${ escapeHtml( String( right.score ) ) }</div>`; 173 174 let statusHtml = ''; 175 if ( isLive ) { 176 statusHtml = `<div class="pb-game__status pb-game__status--live">P${ escapeHtml( String( game.period ) ) } ${ escapeHtml( game.clock ) }</div>`; 177 } else { 178 statusHtml = '<div class="pb-game__status">Final</div>'; 179 } 180 181 // Center column now holds only score + status. Three-stars and TV 182 // broadcasts live as sibling rows below pb-game__row, so they 183 // survive this innerHTML rewrite untouched. 184 center.innerHTML = scoreHtml + statusHtml; 185 186 // When a live game finalizes mid-poll, drop the TV row in favor 187 // of the three-stars row if the API returned them. 188 if ( isFinal && game.three_stars && game.three_stars.length ) { 189 const tvEl = card.querySelector( '.pb-game__tv' ); 190 if ( tvEl ) { 191 tvEl.remove(); 192 } 193 let starsEl = card.querySelector( '.pb-game__stars' ); 194 if ( ! starsEl ) { 195 starsEl = document.createElement( 'div' ); 196 starsEl.className = 'pb-game__stars'; 197 card.appendChild( starsEl ); 198 } 199 starsEl.textContent = formatThreeStars( game.three_stars ); 200 } 201 } 202 } 203 204 /** 205 * Update a team element's name and logo. 206 * 207 * @param {Element} el Team DOM element. 208 * @param {Object} team Team data object. 209 */ 210 function updateTeamEl( el, team ) { 211 const nameEl = el.querySelector( '.pb-game__name' ); 212 if ( nameEl ) { 213 nameEl.textContent = team.abbrev; 214 } 215 const logoEl = el.querySelector( '.pb-game__logo' ); 216 if ( logoEl ) { 217 logoEl.src = team.logo || ''; 218 logoEl.alt = team.abbrev; 219 } 220 } 221 222 /** 223 * Determine hero result label and modifier class for a final game. 224 * 225 * @param {Element} hero The hero DOM element. 226 * @param {Object} game Normalized game data from the REST API. 227 * @return {{ label: string, modifier: string, verb: string, suffix: string }} Result info. 228 */ 229 function getHeroResult( hero, game ) { 230 // Determine if the highlighted team is home or away by reading the 231 // opponent prefix rendered in the hero ("vs" = home, "@" = away). 232 const oppEl = hero.querySelector( '.pb-schedule__hero-opponent span' ); 233 let isHome = true; 234 if ( oppEl ) { 235 const text = oppEl.textContent || ''; 236 if ( text.indexOf( '@' ) === 0 ) { 237 isHome = false; 238 } 239 } 240 241 const teamScore = isHome ? game.home.score : game.away.score; 242 const oppScore = isHome ? game.away.score : game.home.score; 243 const periodType = game.period_type || ''; 244 245 if ( teamScore > oppScore ) { 246 return { label: 'WIN', modifier: 'win', verb: 'beat the', suffix: '' }; 247 } else if ( periodType === 'SO' ) { 248 return { label: 'SO LOSS', modifier: 'ot-loss', verb: 'fall to the', suffix: ' in SO' }; 249 } else if ( periodType === 'OT' || game.period > 3 ) { 250 return { label: 'OT LOSS', modifier: 'ot-loss', verb: 'fall to the', suffix: ' in OT' }; 251 } 252 return { label: 'LOSS', modifier: 'loss', verb: 'lose to the', suffix: '' }; 253 } 254 255 /** 256 * Update the schedule hero card with fresh game data. 257 * 258 * @param {Element} hero Hero card DOM element. 259 * @param {Object} game Normalized game data from the REST API. 260 */ 261 function updateScheduleHero( hero, game ) {
262 const isLive = ( game.state === 'LIVE' || game.state === 'CRIT' ) && game.period > 0; 263 const isFinal = ( game.state === 'FINAL' || game.state === 'OFF' ); 264 265 // Update hero type class â remove old type + result modifiers. 266 hero.classList.remove( 267 'pb-schedule__hero--upcoming', 268 'pb-schedule__hero--final', 269 'pb-schedule__hero--live', 270 'pb-schedule__hero--win', 271 'pb-schedule__hero--loss', 272 'pb-schedule__hero--ot-loss' 273 ); 274 275 let result; 276 if ( isLive ) { 277 hero.classList.add( 'pb-schedule__hero--live' ); 278 } else if ( isFinal ) { 279 hero.classList.add( 'pb-schedule__hero--final' ); 280 result = getHeroResult( hero, game ); 281 hero.classList.add( `pb-schedule__hero--${ result.modifier }` ); 282 } else { 283 hero.classList.add( 'pb-schedule__hero--upcoming' ); 284 } 285 286 // Update label. 287 const label = hero.querySelector( '.pb-schedule__hero-label' ); 288 if ( label ) { 289 if ( isLive ) { 290 label.textContent = 'LIVE'; 291 } else if ( isFinal ) { 292 label.textContent = result ? result.label : 'LAST GAME'; 293 } else { 294 label.textContent = 'NEXT GAME'; 295 } 296 } 297 298 // Update headline text on FINAL transition. 299 if ( isFinal && result ) { 300 const teamShort = hero.getAttribute( 'data-team-short' ) || '';
301 const oppShort = hero.getAttribute( 'data-opp-short' ) || ''; 302 if ( teamShort && oppShort ) { 303 const headlineSpan = hero.querySelector( '.pb-schedule__hero-opponent span' ); 304 if ( headlineSpan ) { 305 headlineSpan.textContent = `${ teamShort } ${ result.verb } ${ oppShort }${ result.suffix }`; 306 } 307 } 308 } 309 310 const body = hero.querySelector( '.pb-schedule__hero-body' ); 311 if ( ! body ) { 312 return; 313 } 314 315 let scoreEl = body.querySelector( '.pb-schedule__hero-score' ); 316 let statusEl = body.querySelector( '.pb-schedule__hero-status' ); 317 318 if ( isLive || isFinal ) { 319 // Leader first for both live and final. 320 const high = Math.max( game.away.score, game.home.score ); 321 const low = Math.min( game.away.score, game.home.score ); 322 const scoreText = `${ high }\u2013${ low }`; 323 324 if ( ! scoreEl ) { 325 scoreEl = document.createElement( 'div' ); 326 scoreEl.className = 'pb-schedule__hero-score'; 327 body.appendChild( scoreEl ); 328 } 329 scoreEl.textContent = scoreText; 330 331 // Build status text. 332 let statusText; 333 let statusClass = 'pb-schedule__hero-status'; 334 if ( isLive ) { 335 statusText = `${ formatPeriod( game.period ) } ${ game.clock || '' }`; 336 statusClass += ' pb-schedule__hero-status--live'; 337 } else { 338 statusText = 'Final'; 339 } 340 341 if ( ! statusEl ) { 342 statusEl = document.createElement( 'div' ); 343 body.appendChild( statusEl ); 344 } 345 statusEl.className = statusClass; 346 statusEl.textContent = statusText; 347 } else { 348 // Upcoming â remove score and status if present. 349 if ( scoreEl ) { 350 scoreEl.remove(); 351 } 352 if ( statusEl ) { 353 statusEl.remove(); 354 } 355 } 356 } 357 358 /** 359 * Escape HTML entities. 360 * 361 * @param {string} str Raw string. 362 * @return {string} HTML-escaped string. 363 */ 364 function escapeHtml( str ) { 365 const div = document.createElement( 'div' ); 366 div.appendChild( document.createTextNode( str ) ); 367 return div.innerHTML; 368 } 369 370 /** 371 * Format three stars data into a display string. 372 * 373 * @param {Array} stars Array of star objects from REST API. 374 * @return {string} Formatted string, e.g. "â Aho (1G) | â â Svechnikov (2A)" 375 */ 376 function formatThreeStars( stars ) { 377 const symbols = { 1: '\u2605', 2: '\u2605\u2605', 3: '\u2605\u2605\u2605' }; 378 const parts = []; 379 380 for ( const star of stars ) { 381 const symbol = symbols[ star.star ] || '';
382 const nameParts = ( star.name || '' ).split( ' ' ); 383 const lastName = nameParts.length > 1 ? nameParts[ nameParts.length - 1 ] : star.name; 384 let statLine; 385 386 if ( star.position === 'G' ) { 387 statLine = star.saves != null 388 ? `${ lastName } (${ star.saves } SV)` 389 : `${ lastName } (G)`; 390 } else { 391 const statParts = []; 392 if ( star.goals > 0 ) { 393 statParts.push( `${ star.goals }G` ); 394 } 395 if ( star.assists > 0 ) { 396 statParts.push( `${ star.assists }A` ); 397 } 398 if ( statParts.length === 0 ) { 399 statParts.push( star.position || '' ); 400 } 401 statLine = `${ lastName } (${ statParts.join( ', ' ) })`; 402 } 403 404 parts.push( `${ symbol } ${ statLine }` ); 405 } 406 407 return parts.join( ' | ' ); 408 } 409 410 /** 411 * Refresh games-today blocks/shortcodes via REST API and update DOM. 412 */ 413 function refreshGames() { 414 if ( ! hasLiveGames() ) { 415 stopAutoRefresh(); 416 return; 417 } 418 419 const url = restEndpoint( 'gamesToday', 'press-box/v1/games-today' ); 420 421 fetch( url ) 422 .then( ( response ) => response.json() ) 423 .then( ( json ) => { 424 if ( ! json.success || ! json.data ) { 425 return; 426 } 427 428 // Build a lookup of game data by game ID. 429 const gamesById = {}; 430 for ( const day of json.data ) { 431 for ( const game of day.games ) { 432 if ( game.id ) { 433 gamesById[ game.id ] = game; 434 } 435 } 436 } 437 438 // Include upcoming games in lookup (defensive). 439 if ( json.upcoming ) { 440 for ( const day of json.upcoming ) { 441 for ( const game of day.games ) { 442 if ( game.id ) { 443 gamesById[ game.id ] = game; 444 } 445 } 446 } 447 } 448 449 // Update each game card that has a matching data-game-id. 450 const cards = document.querySelectorAll( '.pb-game[data-game-id]' ); 451 for ( const card of cards ) { 452 const gameId = card.getAttribute( 'data-game-id' ); 453 if ( gamesById[ gameId ] ) { 454 updateGameCard( card, gamesById[ gameId ] ); 455 } 456 } 457 458 // Update schedule hero if present. 459 const hero = document.querySelector( '.pb-schedule__hero[data-game-id]' ); 460 if ( hero ) { 461 const heroGameId = hero.getAttribute( 'data-game-id' ); 462 if ( gamesById[ heroGameId ] ) { 463 updateScheduleHero( hero, gamesById[ heroGameId ] ); 464 } 465 } 466 } ) 467 .catch( () => { 468 // Silently fail â stale data is still visible. 469 } ); 470 } 471 472 /** 473 * Start auto-refresh polling when live games are detected. 474 */ 475 function startAutoRefresh() { 476 if ( refreshTimer ) { 477 return; 478 } 479 480 if ( hasLiveGames() ) { 481 refreshTimer = setInterval( refreshGames, REFRESH_INTERVAL ); 482 } 483 } 484 485 /** 486 * Stop auto-refresh polling. 487 */ 488 function stopAutoRefresh() { 489 if ( refreshTimer ) { 490 clearInterval( refreshTimer ); 491 refreshTimer = null; 492 } 493 } 494 495 /** 496 * Pause/resume polling based on tab visibility. 497 */ 498 function handleVisibilityChange() { 499 if ( document.hidden ) { 500 stopAutoRefresh(); 501 } else { 502 startAutoRefresh(); 503 } 504 } 505 506 /** 507 * Activate a standings tab by view name. 508 * 509 * @param {Element} container Standings container element. 510 * @param {string} view View name to activate. 511 */ 512 function activateTab( container, view ) { 513 const tabs = container.querySelectorAll( '.pb-standings__tab' ); 514 const panels = container.querySelectorAll( '.pb-standings__panel' ); 515 let found = false; 516 517 for ( const tab of tabs ) { 518 if ( tab.getAttribute( 'data-view' ) === view ) { 519 tab.classList.add( 'pb-standings__tab--active' ); 520 tab.setAttribute( 'aria-selected', 'true' ); 521 found = true; 522 } else { 523 tab.classList.remove( 'pb-standings__tab--active' ); 524 tab.setAttribute( 'aria-selected', 'false' ); 525 } 526 } 527 528 if ( ! found ) { 529 return; 530 } 531 532 for ( const panel of panels ) { 533 if ( panel.getAttribute( 'data-panel' ) === view ) { 534 panel.removeAttribute( 'hidden' ); 535 } else { 536 panel.setAttribute( 'hidden', '' ); 537 } 538 } 539 } 540 541 /** 542 * Initialize standings tab switching. 543 */ 544 function initStandingsTabs() { 545 const containers = document.querySelectorAll( '.pb-standings' ); 546 547 for ( const container of containers ) { 548 const tabs = container.querySelectorAll( '.pb-standings__tab' ); 549 550 // Restore saved tab from localStorage. 551 try { 552 const saved = localStorage.getItem( 'pb_standings_view' ); 553 if ( saved ) { 554 activateTab( container, saved ); 555 } 556 } catch ( e ) { 557 // localStorage may be unavailable. 558 } 559 560 for ( const tab of tabs ) { 561 tab.addEventListener( 'click', () => { 562 const view = tab.getAttribute( 'data-view' ); 563 activateTab( container, view ); 564 try { 565 localStorage.setItem( 'pb_standings_view', view ); 566 } catch ( e ) { 567 // localStorage may be unavailable. 568 } 569 } ); 570 } 571 } 572 } 573
574 /** 575 * Initialize sortable table headers. 576 * 577 * Works on any table containing th[data-sort] headers. 578 * Text vs numeric: add data-sort-type="text" on <th> for text columns (default: numeric). 579 * Initial sorted column: add class="pb-sortable__th--sorted" and data-sort-dir="asc|desc". 580 */ 581 function pbInitTableSort( root ) { 582 const scope = root || document; 583 const allTables = scope.querySelectorAll( 'table' ); 584 585 for ( const table of allTables ) { 586 const headers = table.querySelectorAll( 'th[data-sort]' ); 587 if ( ! headers.length ) { 588 continue; 589 } 590 591 const thead = table.querySelector( 'thead' ); 592 const tbody = table.querySelector( 'tbody' ); 593 if ( ! thead || ! tbody ) { 594 continue; 595 } 596 597 // Build column index map: data-sort key â column index. 598 const allTh = thead.querySelectorAll( 'th' ); 599 const colIndex = {}; 600 allTh.forEach( ( th, idx ) => { 601 const key = th.getAttribute( 'data-sort' ); 602 if ( key ) { 603 colIndex[ key ] = idx; 604 } 605 } ); 606 607 for ( const th of headers ) { 608 th.addEventListener( 'click', () => { 609 const key = th.getAttribute( 'data-sort' ); 610 const idx = colIndex[ key ]; 611 if ( idx === undefined ) { 612 return; 613 } 614 615 // Determine direction: toggle if same column, else default to asc. 616 const currentDir = th.getAttribute( 'data-sort-dir' ); 617 const dir = currentDir 618 ? ( currentDir === 'asc' ? 'desc' : 'asc' ) 619 : 'asc'; 620 621 // Clear sorted state from all sortable headers in this table. 622 for ( const h of headers ) { 623 h.classList.remove( 'pb-sortable__th--sorted' ); 624 h.removeAttribute( 'data-sort-dir' ); 625 } 626 627 // Set active state. 628 th.classList.add( 'pb-sortable__th--sorted' ); 629 th.setAttribute( 'data-sort-dir', dir ); 630 631 // Sort rows â exclude cutoff rows (e.g. standings wildcard line). 632 const rows = Array.from( 633 tbody.querySelectorAll( 'tr:not(.pb-standings__cutoff)' ) 634 ); 635 const attrName = `data-sort-${ key }`; 636 const isText = th.getAttribute( 'data-sort-type' ) === 'text'; 637 638 rows.sort( ( a, b ) => { 639 const aCell = a.children[ idx ]; 640 const bCell = b.children[ idx ]; 641 const aVal = aCell ? aCell.getAttribute( attrName ) || '' : ''; 642 const bVal = bCell ? bCell.getAttribute( attrName ) || '' : ''; 643 644 let cmp; 645 if ( isText ) { 646 cmp = aVal.localeCompare( bVal ); 647 } else { 648 cmp = parseFloat( aVal ) - parseFloat( bVal ); 649 } 650 651 return dir === 'asc' ? cmp : -cmp; 652 } ); 653 654 // Remove cutoff rows (irrelevant once user sorts manually). 655 const cutoffs = tbody.querySelectorAll( '.pb-standings__cutoff' ); 656 for ( const row of cutoffs ) { 657 row.remove(); 658 } 659 660 // Re-append sorted rows. 661 for ( const row of rows ) { 662 tbody.appendChild( row ); 663 } 664 } ); 665 } 666 } 667 } 668 669 /** 670 * Initialize Team Tag Page tab switching + lazy panel loading. 671 * 672 * The "Today" panel is server-rendered. The other five panels are fetched 673 * from /wp-json/press-box/v1/team-tag/panel on first activation, then 674 * cached in-memory on the panel element so re-clicking is instant. 675 */ 676 function initTeamTagTabs() { 677 const containers = document.querySelectorAll( '.pb-team-tag' ); 678 679 for ( const container of containers ) { 680 const team = container.getAttribute( 'data-team' ); 681 const tabs = container.querySelectorAll( '[data-pb-tab]' ); 682 const panels = container.querySelectorAll( '[data-pb-panel]' ); 683 const panelUrl = restEndpoint( 'teamTagPanel', 'press-box/v1/team-tag/panel' ); 684 685 if ( ! team || ! tabs.length || ! panels.length ) { 686 continue; 687 } 688 689 function activate( key ) { 690 for ( const tab of tabs ) { 691 const on = tab.getAttribute( 'data-pb-tab' ) === key; 692 tab.classList.toggle( 'pb-team-tag__tab--active', on ); 693 tab.setAttribute( 'aria-selected', on ? 'true' : 'false' ); 694 } 695 for ( const panel of panels ) { 696 const on = panel.getAttribute( 'data-pb-panel' ) === key; 697 panel.classList.toggle( 'pb-team-tag__panel--active', on ); 698 if ( on ) { 699 panel.removeAttribute( 'hidden' ); 700 } else { 701 panel.setAttribute( 'hidden', '' ); 702 } 703 } 704 } 705 706 function load( key, panel ) { 707 if ( panel.dataset.pbLoaded === '1' ) { 708 return; 709 } 710 panel.dataset.pbLoaded = '1'; 711 712 const url = withParams( panelUrl, { team: team, panel: key } ); 713 714 fetch( url, { credentials: 'same-origin', headers: { Accept: 'application/json' } } ) 715 .then( ( r ) => r.ok ? r.json() : Promise.reject( r.status ) ) 716 .then( ( json ) => { 717 if ( json && json.success && json.data && typeof json.data.html === 'string' ) { 718 panel.innerHTML = json.data.html; 719 convertToLocalTimes( panel ); 720 pbInitTableSort( panel ); 721 } else { 722 panel.innerHTML = '<p class="pb-team-tag__panel-error">Content unavailable.</p>'; 723 panel.dataset.pbLoaded = '0'; 724 } 725 } ) 726 .catch( () => { 727 panel.innerHTML = '<p class="pb-team-tag__panel-error">Content unavailable.</p>'; 728 panel.dataset.pbLoaded = '0'; 729 } ); 730 } 731 732 for ( const tab of tabs ) { 733 tab.addEventListener( 'click', () => {
734 const key = tab.getAttribute( 'data-pb-tab' ); 735 const panel = container.querySelector( '[data-pb-panel="' + key + '"]' ); 736 activate( key ); 737 if ( panel && key !== 'today' ) { 738 load( key, panel ); 739 } 740 } ); 741 } 742 } 743 } 744 745 /** 746 * Initialize on DOM ready. 747 */ 748 function init() { 749 convertToLocalTimes(); 750 startAutoRefresh(); 751 initStandingsTabs(); 752 initTeamTagTabs(); 753 pbInitTableSort(); 754 755 // Fetch fresh data immediately if games are (or should be) live, 756 // rather than waiting for the first 30-second polling interval. 757 refreshGames(); 758 759 document.addEventListener( 'visibilitychange', handleVisibilityChange ); 760 } 761 762 if ( document.readyState === 'loading' ) { 763 document.addEventListener( 'DOMContentLoaded', init ); 764 } else { 765 init(); 766 } 767} )();
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.