1/** 2 * SPA è·¯ç±å¨ - å®ç°æ å·æ°é¡µé¢åæ¢ 3 * ç¹å»å¯¼èªæ¶åªæ¿æ¢ä¸»å 容åºåï¼ä¸æ´é¡µå·æ° 4 */ 5(function() { 6 'use strict'; 7 8 const MAIN_CONTENT_ID = 'main-content'; 9 const TRANSITION_DURATION = 300; 10 const SKELETON_DELAY_MS = 320; 11 const ARTICLE_LIST_PATHS = [ 12 '/tin-tuc-bong-da', 13 '/news', 14 '/xinwen', 15 '/nhan-dinh-bong-da', 16 '/analysis', 17 '/fenxi', 18 '/articles', 19 '/article/news', 20 '/article/analysis' 21 ]; 22 23 // éè¦ SPA å¤çç龿¥éæ©å¨ 24 const SPA_LINK_SELECTORS = [ 25 '.navbar-nav .nav-link', 26 '.mobile-nav .nav-link', 27 '.footer-links a', 28 '.news-card', 29 '.match-card a', 30 '.league-item', 31 'a[data-spa]' 32 ]; 33 34 // æé¤ç龿¥ï¼ç»å½ã注éãå¤é¨é¾æ¥çï¼ 35 const EXCLUDE_PATTERNS = [ 36 '/Account/', 37 '/Admin/', 38 '/admin/', 39 'logout', 40 'login', 41 'register', 42 '#', 43 'javascript:', 44 'mailto:', 45 'tel:' 46 ]; 47 48 let isNavigating = false; 49 let abortController = null; 50 let skeletonTimer = null; 51 let skeletonShown = false; 52 let detailNavLock = false; 53 54 /** 55 * åå§å SPA è·¯ç± 56 */ 57 function init() { 58 // æ¯èµè¯¦æ 卿è·é¶æ®µæ¦æªï¼é¿å SPA å æ¹ URL å´ä¸æ¢é¡µé¢ã 59 document.addEventListener('click', handleMatchDetailClick, true); 60 document.addEventListener('click', handleLinkClick); 61 62 // ç嬿µè§å¨åè¿/åé 63 window.addEventListener('popstate', handlePopState); 64 resetMatchDetailScroll(); 65 window.addEventListener('pageshow', resetMatchDetailScroll); 66 ensureMatchListSearch(); 67 ensureFooterDirectoryLinks(); 68 localizeDirectoryPagination(); 69 alignListSearch(); 70 document.addEventListener('keydown', onListSearchEnter, true); 71 document.addEventListener('keypress', onListSearchEnter, true); 72 document.addEventListener('focusin', holdSearchScroll); 73 document.addEventListener('focusout', releaseSearchScroll); 74 document.addEventListener('input', rememberListFilterScroll, true); 75 document.addEventListener('input', restoreListFilterScroll); 76 document.addEventListener('spa:loaded', ensureMatchListSearch); 77 document.addEventListener('spa:loaded', ensureFooterDirectoryLinks); 78 document.addEventListener('spa:loaded', localizeDirectoryPagination); 79 document.addEventListener('spa:loaded', alignListSearch); 80 window.addEventListener('load', alignListSearch); 81 82 console.log('SPA Router initialized'); 83 } 84 85 function footerDirectoryLabels() { 86 const lang = String(window.__currentLang || document.documentElement.lang || 'vi').toLowerCase(); 87 if (lang.indexOf('zh') === 0) return { league: 'èèµä¸å¿', team: 'çéä¸å¿' }; 88 if (lang.indexOf('en') === 0) return { league: 'League Center', team: 'Team Center' }; 89 return { league: 'Trung tâm giải Äấu', team: 'Trung tâm câu lạc bá»' }; 90 } 91 92 function ensureFooterDirectoryLinks() { 93 const list = document.querySelector('.footer .footer-links, .footer-links'); 94 if (!list) return; 95 const labels = footerDirectoryLabels(); 96 const items = [ 97 { href: window.__leagueRoutePrefix || '/giai-dau', text: labels.league }, 98 { href: window.__teamRoutePrefix || '/doi-bong', text: labels.team } 99 ]; 100 items.forEach(function (item) { 101 const href = item.href || ''; 102 if (!href) return; 103 const exists = Array.prototype.some.call(list.querySelectorAll('a'), function (link) { 104 return link.getAttribute('href') === href; 105 }); 106 if (exists) return; 107 const li = document.createElement('li'); 108 const a = document.createElement('a'); 109 a.href = href; 110 a.textContent = item.text; 111 li.appendChild(a); 112 list.appendChild(li); 113 }); 114 } 115 116 function matchListCopy() { 117 const lang = String(window.__currentLang || document.documentElement.lang || 'vi').toLowerCase(); 118 if (lang.indexOf('zh') === 0) { 119 return { search: 'æç´¢', placeholder: 'æç´¢çéæèèµ...', empty: 'æªæ¾å°ç»æ', clear: 'æ¸ é¤' }; 120 } 121 if (lang.indexOf('en') === 0) { 122 return { search: 'Search', placeholder: 'Search teams or leagues...', empty: 'No results found', clear: 'Clear' }; 123 } 124 return { search: 'Tìm kiếm', placeholder: 'Tìm kiếm Äá»i bóng hoặc giải Äấu...', empty: 'Không tìm thấy kết quả', clear: 'Xóa' }; 125 } 126 127 function ensureMatchListSearch() { 128 const page = document.querySelector('.match-list-page'); 129 if (!page) return;
130 const row = page.querySelector('.filter-bar-row'); 131 if (!row) return; 132 133 const copy = matchListCopy(); 134 let tools = row.querySelector('.filter-bar-tools'); 135 const dropdown = row.querySelector('.schedule-category-dropdown'); 136 if (!tools) { 137 tools = document.createElement('div'); 138 tools.className = 'filter-bar-tools'; 139 if (dropdown && dropdown.parentNode) { 140 dropdown.parentNode.insertBefore(tools, dropdown); 141 tools.appendChild(dropdown); 142 } else { 143 row.appendChild(tools); 144 } 145 } 146 147 if (!document.getElementById('matchListSearch')) { 148 const wrap = document.createElement('div'); 149 wrap.className = 'match-list-search'; 150 wrap.innerHTML = 151 '<label class="visually-hidden" for="matchListSearch"></label>' + 152 '<svg class="match-list-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg>' + 153 '<input id="matchListSearch" class="match-list-search-input" type="search" autocomplete="off" enterkeyhint="search">' + 154 '<button type="button" class="match-list-search-clear" id="matchListSearchClear" hidden>×</button>'; 155 wrap.querySelector('label').textContent = copy.search; 156 wrap.querySelector('input').placeholder = copy.placeholder; 157 wrap.querySelector('button').setAttribute('aria-label', copy.clear); 158 const dropdownInTools = tools.querySelector('.schedule-category-dropdown'); 159 if (dropdownInTools) tools.insertBefore(wrap, dropdownInTools); 160 else tools.appendChild(wrap); 161 } 162 163 if (!document.getElementById('matchListSearchEmpty')) { 164 const empty = document.createElement('div'); 165 empty.id = 'matchListSearchEmpty'; 166 empty.className = 'schedule-sport-empty'; 167 empty.hidden = true; 168 empty.textContent = copy.empty; 169 const bar = page.querySelector('.filter-bar'); 170 if (bar && bar.parentNode) bar.parentNode.insertBefore(empty, bar.nextSibling); 171 } 172 173 page.querySelectorAll('nav .pagination').forEach(function (list) { 174 const nav = list.closest('nav'); 175 if (!nav) return; 176 nav.classList.add('match-list-paging'); 177 const next = nav.nextElementSibling; 178 if (next && next.classList.contains('text-muted')) next.classList.add('match-list-paging'); 179 }); 180 181 bindMatchListSearch(page); 182 } 183 184 function matchRowHaystack(row) { 185 const named = Array.prototype.map.call( 186 row.querySelectorAll('.mx-team-name, .mx-streamer-name, .lg-name, .beauty-schedule-title, .beauty-schedule-streamer'), 187 function (el) { return el.textContent || ''; } 188 ).join(' '); 189 return ((row.getAttribute('data-search') || '') + ' ' + named).toLowerCase(); 190 } 191 192 function bindMatchListSearch(page) { 193 const input = document.getElementById('matchListSearch'); 194 const clearBtn = document.getElementById('matchListSearchClear'); 195 const empty = document.getElementById('matchListSearchEmpty'); 196 if (!page || !input || input.dataset.bound === '1') return; 197 input.dataset.bound = '1'; 198 199 function applyFilter() { 200 const query = input.value.trim().toLowerCase(); 201 if (clearBtn) clearBtn.hidden = query.length === 0; 202 let visible = 0; 203 204 page.querySelectorAll('.mx-row').forEach(function (row) { 205 const show = !query || matchRowHaystack(row).indexOf(query) !== -1; 206 row.classList.toggle('is-search-hidden', !show); 207 if (show) visible += 1; 208 }); 209 210 page.querySelectorAll('.lg-section').forEach(function (section) { 211 const any = section.querySelector('.mx-row:not(.is-search-hidden)'); 212 section.classList.toggle('is-search-hidden', !any); 213 section.classList.toggle('is-search-open', !!query && !!any); 214 }); 215 216 page.querySelectorAll('.beauty-schedule-row').forEach(function (row) { 217 const show = !query || matchRowHaystack(row).indexOf(query) !== -1; 218 row.classList.toggle('is-search-hidden', !show); 219 if (show) visible += 1; 220 }); 221 222 page.querySelectorAll('.schedule-sport-section').forEach(function (section) { 223 if (!section.querySelector('.beauty-schedule-row')) return; 224 const any = section.querySelector('.beauty-schedule-row:not(.is-search-hidden)'); 225 section.classList.toggle('is-search-hidden', !any); 226 }); 227 228 page.querySelectorAll('.schedule-sport-title-inline').forEach(function (title) { 229 const anySports = page.querySelector('.lg-section:not(.is-search-hidden)'); 230 title.classList.toggle('is-search-hidden', !!query && !anySports); 231 }); 232 233 page.querySelectorAll('.match-list-paging').forEach(function (el) { 234 el.classList.toggle('is-search-hidden', query.length > 0); 235 }); 236 237 if (empty) empty.hidden = !query || visible > 0; 238 } 239 240 input.addEventListener('input', applyFilter); 241 input.addEventListener('keydown', function (e) { 242 if (e.key === 'Enter') e.preventDefault(); 243 if (e.key === 'Escape') { 244 input.value = ''; 245 applyFilter(); 246 } 247 }); 248 if (clearBtn) { 249 clearBtn.addEventListener('click', function () { 250 input.value = ''; 251 applyFilter(); 252 input.focus(); 253 }); 254 } 255 } 256 257 const LIST_SEARCH_SELECTOR = '#articleSearch, #leagueSearch, #teamSearch'; 258 let preserveSearchAnchor = false;
259 let searchNavKeepsScroll = false; 260 261 function listSearchInput() { 262 return document.querySelector(LIST_SEARCH_SELECTOR); 263 } 264 265 function isListSearchInput(node) { 266 return !!(node && node.matches && node.matches(LIST_SEARCH_SELECTOR + ', #matchListSearch, .match-list-search-input, .match-search-box input')); 267 } 268 269 function setInstantScroll() { 270 const root = document.documentElement; 271 const previous = root.style.scrollBehavior; 272 root.style.scrollBehavior = 'auto'; 273 return function restore() { 274 root.style.scrollBehavior = previous; 275 }; 276 } 277 278 function listFilterMarker(input) { 279 const page = input.closest('.match-list-page') || document; 280 return page.querySelector('.seo-page-h1') || input.closest('.filter-bar') || input; 281 } 282 283 function rememberListFilterScroll(e) { 284 const input = e.target; 285 if (!input || !input.matches || !input.matches('#matchListSearch, .match-list-search-input')) return; 286 const marker = listFilterMarker(input); 287 input._searchScrollMarker = marker; 288 input._searchScrollBefore = marker.getBoundingClientRect().top; 289 input._searchScrollY = window.scrollY; 290 document.documentElement.style.scrollBehavior = 'auto'; 291 document.documentElement.style.overflowAnchor = 'none'; 292 const page = input.closest('.match-list-page'); 293 if (page) page.style.overflowAnchor = 'none'; 294 } 295 296 function restoreListFilterScroll(e) { 297 const input = e.target; 298 if (!input || input._searchScrollBefore == null) return; 299 const before = input._searchScrollBefore; 300 const savedY = input._searchScrollY || 0; 301 const marker = input._searchScrollMarker; 302 input._searchScrollMarker = null; 303 input._searchScrollBefore = null; 304 const page = input.closest('.match-list-page'); 305 if (page) { 306 if ((input.value || '').trim()) { 307 const needed = savedY + window.innerHeight; 308 if (page.getBoundingClientRect().height < needed) page.style.minHeight = needed + 'px'; 309 } else { 310 page.style.minHeight = ''; 311 } 312 } 313 document.documentElement.style.overflowAnchor = ''; 314 if (!marker || !marker.isConnected) return; 315 const delta = marker.getBoundingClientRect().top - before; 316 if (Math.abs(delta) > 1) window.scrollBy(0, delta); 317 } 318 319 function holdSearchScroll(e) { 320 if (!isListSearchInput(e.target)) return; 321 document.documentElement.style.scrollBehavior = 'auto'; 322 } 323 324 function releaseSearchScroll(e) { 325 if (!isListSearchInput(e.target)) return; 326 document.documentElement.style.scrollBehavior = ''; 327 } 328 329 function alignListSearch() { 330 const params = new URLSearchParams(window.location.search); 331 if (!params.get('search')) return; 332 const input = listSearchInput(); 333 if (!input) return; 334 const box = input.closest('.article-latest-head, .section-header') || input; 335 const rect = box.getBoundingClientRect(); 336 const offset = 16; 337 if (rect.top >= offset && rect.bottom <= window.innerHeight - 8) return; 338 const restoreBehavior = setInstantScroll(); 339 window.scrollTo(0, Math.max(0, rect.top + window.scrollY - offset)); 340 restoreBehavior(); 341 } 342 343 function onListSearchEnter(e) { 344 if (e.key !== 'Enter' || e.repeat) return; 345 const input = e.target; 346 if (!input || !input.matches || !input.matches(LIST_SEARCH_SELECTOR)) return; 347 e.preventDefault(); 348 e.stopPropagation(); 349 if (e.type !== 'keydown') return; 350 const value = input.value.trim(); 351 const url = new URL(window.location.href); 352 if (value) url.searchParams.set('search', value); 353 else url.searchParams.delete('search'); 354 url.searchParams.delete('page'); 355 const next = url.pathname + url.search; 356 if (next === window.location.pathname + window.location.search) { 357 alignListSearch(); 358 return; 359 } 360 preserveSearchAnchor = true; 361 navigateTo(next); 362 } 363 364 function directoryPageCopy() { 365 const lang = String(window.__currentLang || document.documentElement.lang || 'vi').toLowerCase(); 366 const zh = lang.indexOf('zh') === 0; 367 const en = lang.indexOf('en') === 0; 368 return { 369 pagination: zh ? 'å页导èª' : (en ? 'Pagination' : 'Äiá»u hưá»ng trang'), 370 first: zh ? 'é¦é¡µ' : (en ? 'First page' : 'Trang Äầu'), 371 prev: zh ? 'ä¸ä¸é¡µ' : (en ? 'Previous Page' : 'Trang trưá»c'), 372 next: zh ? 'ä¸ä¸é¡µ' : (en ? 'Next Page' : 'Trang sau'), 373 last: zh ? 'æ«é¡µ' : (en ? 'Last Page' : 'Trang cuá»i'), 374 status: function (current, total, count, isTeam) { 375 if (zh) return '第 ' + current + ' / ' + total + ' · å ± ' + count + (isTeam ? ' 个çé' : ' 个èèµ'); 376 if (en) return 'Page ' + current + ' / ' + total + ' · ' + count + (isTeam ? ' teams' : ' leagues'); 377 return 'Trang ' + current + ' / ' + total + ' · Tá»ng ' + count + (isTeam ? ' Äá»i bóng' : ' giải Äấu'); 378 } 379 }; 380 } 381 382 function isTeamCenterPath(pathname) { 383 const path = (pathname || '').replace(/\/+$/, '') || '/'; 384 const team = String(window.__teamRoutePrefix || '/doi-bong').replace(/\/+$/, ''); 385 return path === team || path === '/doi-bong' || path === '/teams'; 386 } 387 388 function localizeDirectoryPagination() { 389 if (!isDirectoryCenterPath(window.location.pathname)) return; 390 const isTeam = isTeamCenterPath(window.location.pathname); 391 const copy = directoryPageCopy(); 392 const iconTitles = { 393 'fa-angle-double-left': copy.first, 394 'fa-angle-left': copy.prev, 395 'fa-angle-right': copy.next, 396 'fa-angle-double-right': copy.last 397 }; 398 document.querySelectorAll('#main-content nav .pagination').forEach(function (list) { 399 const nav = list.closest('nav'); 400 if (nav) nav.setAttribute('aria-label', copy.pagination);
401 list.querySelectorAll('a.page-link i').forEach(function (icon) { 402 Object.keys(iconTitles).forEach(function (cls) { 403 if (icon.classList.contains(cls)) { 404 const link = icon.closest('a'); 405 if (link) link.setAttribute('title', iconTitles[cls]); 406 } 407 }); 408 }); 409 const summary = nav && nav.nextElementSibling ? nav.nextElementSibling.querySelector('small') : null; 410 if (!summary) return; 411 const nums = (summary.textContent || '').match(/\d[\d,]*/g); 412 if (!nums || nums.length < 3) return; 413 summary.textContent = copy.status( 414 nums[0].replace(/,/g, ''), 415 nums[1].replace(/,/g, ''), 416 nums[nums.length - 1].replace(/,/g, ''), 417 isTeam 418 ); 419 }); 420 } 421 422 function isDirectoryCenterPath(pathname) { 423 const path = (pathname || '').replace(/\/+$/, '') || '/'; 424 const known = ['/giai-dau', '/doi-bong', '/leagues', '/teams', window.__leagueRoutePrefix, window.__teamRoutePrefix]; 425 return known.some(function (item) { 426 const normalized = (item || '').replace(/\/+$/, ''); 427 return normalized && path === normalized; 428 }); 429 } 430 431 function isMatchDetailPath(pathname) { 432 return /-id\d+(?:\/|$)/i.test(pathname) 433 || pathname.indexOf('/live/beauty/') === 0 434 || pathname.indexOf('/live/football/') === 0 435 || pathname.indexOf('/Match/Details') === 0; 436 } 437 438 function isDirectoryCenterHref(href) { 439 if (!href || href.charAt(0) === '#' || href.indexOf('javascript:') === 0) return false; 440 try { 441 const url = new URL(href, window.location.href); 442 if (url.origin !== window.location.origin) return false; 443 return isDirectoryCenterPath(url.pathname); 444 } catch (e) { 445 return false; 446 } 447 } 448 449 function resetMatchDetailScroll() { 450 const path = window.location.pathname; 451 if (!isMatchDetailPath(path) && !isDirectoryCenterPath(path)) return; 452 window.scrollTo(0, 0); 453 } 454 455 function isMatchDetailHref(href) { 456 if (!href || href.charAt(0) === '#' || href.indexOf('javascript:') === 0) return false; 457 try { 458 const url = new URL(href, window.location.href); 459 if (url.origin !== window.location.origin) return false; 460 return isMatchDetailPath(url.pathname); 461 } catch (e) { 462 return false; 463 } 464 } 465 466 function matchUrlFromCard(card) { 467 if (!card) return ''; 468 const bodyLink = card.querySelector('.match-body-link, a.focus-match-card-link'); 469 if (bodyLink && bodyLink.href) return bodyLink.href; 470 if (card.tagName === 'A' && card.href) return card.href; 471 return card.getAttribute('data-match-url') || ''; 472 } 473 474 function shouldInterceptPrimaryClick(e) { 475 return e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey; 476 } 477 478 function handleMatchDetailClick(e) { 479 if (!shouldInterceptPrimaryClick(e)) return; 480 481 const watchBtn = e.target.closest('.watch-btn'); 482 if (watchBtn) { 483 const href = matchUrlFromCard(watchBtn.closest('.match-card, .focus-match-card, .focus-card')); 484 if (!isMatchDetailHref(href)) return; 485 e.preventDefault(); 486 e.stopPropagation(); 487 showMatchDetailSkeletonAndGo(href); 488 return; 489 } 490 491 const link = e.target.closest('a'); 492 if (!link || link.target === '_blank') return; 493 const href = link.getAttribute('href') || link.href; 494 if (isDirectoryCenterHref(href)) { 495 e.preventDefault(); 496 e.stopPropagation(); 497 showSkeletonAndGo(link.href, directorySkeletonMarkup()); 498 return; 499 } 500 if (!isMatchDetailHref(href)) return; 501 e.preventDefault(); 502 e.stopPropagation(); 503 showMatchDetailSkeletonAndGo(link.href); 504 } 505
506 function matchDetailSkeletonMarkup() { 507 const label = (document.body && document.body.getAttribute('data-loading-text')) || 'Loading'; 508 return ` 509 <div class="container-fluid match-detail-skeleton" aria-busy="true" aria-live="polite"> 510 <span class="visually-hidden">${label}</span> 511 <div class="mds-crumb mds-shimmer"></div> 512 <div class="row"> 513 <div class="col-lg-8"> 514 <div class="mds-card mds-player"> 515 <div class="mds-player-head"> 516 <div class="mds-shimmer mds-pill"></div> 517 <div class="mds-shimmer mds-pill mds-pill-short"></div> 518 </div> 519 <div class="mds-shimmer mds-screen"></div> 520 </div> 521 <div class="mds-card mds-hero"> 522 <div class="mds-shimmer mds-league"></div> 523 <div class="mds-hero-row"> 524 <div class="mds-team"><div class="mds-shimmer mds-logo"></div><div class="mds-shimmer mds-name"></div></div> 525 <div class="mds-shimmer mds-score"></div> 526 <div class="mds-team"><div class="mds-shimmer mds-logo"></div><div class="mds-shimmer mds-name"></div></div> 527 </div> 528 </div> 529 <div class="mds-card mds-tabs"> 530 <div class="mds-tabbar"> 531 <div class="mds-shimmer"></div> 532 <div class="mds-shimmer"></div> 533 <div class="mds-shimmer"></div> 534 <div class="mds-shimmer"></div> 535 </div> 536 <div class="mds-shimmer mds-line"></div> 537 <div class="mds-shimmer mds-line mds-line-lg"></div> 538 <div class="mds-shimmer mds-line"></div> 539 <div class="mds-shimmer mds-line mds-line-md"></div> 540 </div> 541 </div> 542 <div class="col-lg-4"> 543 <div class="mds-card mds-side"> 544 <div class="mds-shimmer mds-side-head"></div> 545 <div class="mds-side-lines"> 546 <div class="mds-shimmer mds-line"></div> 547 <div class="mds-shimmer mds-line mds-line-md"></div> 548 <div class="mds-shimmer mds-line"></div> 549 <div class="mds-shimmer mds-line mds-line-lg"></div> 550 <div class="mds-shimmer mds-line"></div> 551 </div> 552 <div class="mds-shimmer mds-side-input"></div> 553 </div> 554 </div> 555 </div> 556 </div>`; 557 } 558 559 function directoryCardMarkup() { 560 return ` 561 <div class="col-md-6 col-lg-4"> 562 <div class="dir-skel-card"> 563 <div class="mds-shimmer dir-skel-logo"></div> 564 <div class="dir-skel-text"> 565 <div class="mds-shimmer dir-skel-line"></div> 566 <div class="mds-shimmer dir-skel-line dir-skel-line-short"></div> 567 </div> 568 <div class="mds-shimmer dir-skel-badge"></div> 569 </div> 570 </div>`; 571 } 572 573 function directorySkeletonMarkup() { 574 const label = (document.body && document.body.getAttribute('data-loading-text')) || 'Loading'; 575 const card = directoryCardMarkup(); 576 return ` 577 <div class="container-fluid directory-skeleton pt-3 pb-4" aria-busy="true" aria-live="polite"> 578 <span class="visually-hidden">${label}</span> 579 <div class="dir-skel-header"> 580 <div class="mds-shimmer dir-skel-title"></div> 581 <div class="mds-shimmer dir-skel-search"></div> 582 </div> 583 <div class="mds-shimmer dir-skel-section"></div> 584 <div class="row g-3 mb-4">${card}${card}${card}${card}${card}${card}</div> 585 <div class="mds-shimmer dir-skel-section"></div> 586 <div class="row g-3">${card}${card}${card}${card}${card}${card}${card}${card}${card}</div> 587 </div>`; 588 } 589 590 function showSkeletonAndGo(url, markup) { 591 if (detailNavLock) return; 592 detailNavLock = true; 593 window.scrollTo(0, 0); 594 const mainContent = document.getElementById(MAIN_CONTENT_ID); 595 if (mainContent) { 596 mainContent.innerHTML = markup; 597 } 598 requestAnimationFrame(function () { 599 requestAnimationFrame(function () { 600 window.location.assign(url); 601 }); 602 }); 603 } 604 605 function showMatchDetailSkeletonAndGo(url) { 606 showSkeletonAndGo(url, matchDetailSkeletonMarkup()); 607 } 608 609 /** 610 * æ£æ¥é¾æ¥æ¯å¦éè¦ SPA å¤ç 611 */ 612 function shouldHandleLink(link) { 613 const href = link.getAttribute('href'); 614 615 if (!href) return false;
616 617 // æ¯èµç¸å ³é¡µé¢éè¦å®æ´å·æ°ï¼å å«å èèæ¬éè¦éæ°åå§åï¼ 618 // - æ¯èµè¯¦æ 页: è天室 SignalR åå§å 619 // - æ¯èµå表页: èæåè¡¨ãæ éæ»å¨çJSåå§å 620 if (href.startsWith('/Match/Details') || 621 href.match(/^\/match\/\d+/) || 622 href.match(/^\/football\/\d+/) || 623 href.startsWith('/football/') || 624 href.startsWith('/matches/') || 625 href.startsWith('/Match') || 626 href.startsWith('/streamer/')) { 627 return false; 628 } 629 630 // æ£æ¥æé¤æ¨¡å¼ 631 for (const pattern of EXCLUDE_PATTERNS) { 632 if (href.includes(pattern)) return false; 633 } 634 635 // å¤é¨é¾æ¥ 636 if (href.startsWith('http') && !href.includes(window.location.host)) { 637 return false; 638 } 639 640 // æ°çªå£æå¼ 641 if (link.target === '_blank') return false; 642 643 // æ£æ¥æ¯å¦å¹é SPA éæ©å¨ 644 for (const selector of SPA_LINK_SELECTORS) { 645 if (link.matches(selector) || link.closest(selector)) { 646 return true; 647 } 648 } 649 650 // å页çåä¸è·¯å¾çæ¥è¯¢é¾æ¥ 651 if (href.startsWith('?')) { 652 return true; 653 } 654 655 // å ¶ä»ç«å 龿¥ä¹èµ° SPA 656 if (href.startsWith('/') && !href.startsWith('//')) { 657 return true; 658 } 659 660 return false; 661 } 662 663 /** 664 * å¤ç龿¥ç¹å» 665 */ 666 function handleLinkClick(e) { 667 const link = e.target.closest('a'); 668 if (!link) return; 669 670 const linkHref = link.getAttribute('href') || link.href; 671 if (isDirectoryCenterHref(linkHref)) { 672 if (!shouldInterceptPrimaryClick(e) || link.target === '_blank') return; 673 e.preventDefault(); 674 showSkeletonAndGo(link.href, directorySkeletonMarkup()); 675 return; 676 } 677 if (isMatchDetailHref(linkHref)) { 678 if (!shouldInterceptPrimaryClick(e) || link.target === '_blank') return; 679 e.preventDefault(); 680 showMatchDetailSkeletonAndGo(link.href); 681 return; 682 } 683 684 if (!shouldHandleLink(link)) return; 685 686 e.preventDefault(); 687 688 const url = link.getAttribute('href'); 689 navigateTo(url); 690 } 691 692 /** 693 * å¤çæµè§å¨åè¿/åé 694 */ 695 function handlePopState(e) { 696 if (e.state && e.state.spa) { 697 loadPage(window.location.pathname + window.location.search, false); 698 } 699 } 700 701 /** 702 * 导èªå°æ°é¡µé¢ 703 */ 704 function navigateTo(url) { 705 if (isNavigating) return; 706 if (url === window.location.pathname + window.location.search) return; 707 708 // æ´æ° URL 709 history.pushState({ spa: true }, '', url); 710 711 // å è½½é¡µé¢ 712 loadPage(url, true); 713 } 714 715 /** 716 * å 载页é¢å 容 717 */ 718 async function loadPage(url, showTransition = true) { 719 const mainContent = document.getElementById(MAIN_CONTENT_ID); 720 if (!mainContent) { 721 // 没æä¸»å 容åºåï¼åéå°ä¼ ç»å·æ° 722 window.location.href = url; 723 return; 724 } 725 726 isNavigating = true; 727 searchNavKeepsScroll = preserveSearchAnchor; 728 preserveSearchAnchor = false; 729 const articleListNav = isArticleListUrl(url); 730 731 // åæ¶ä¹åçè¯·æ± 732 if (abortController) { 733 abortController.abort(); 734 } 735 abortController = new AbortController(); 736 737 try { 738 // æ¾ç¤ºå è½½ç¶æãæç« å表å ä¿çå½å页é¢ï¼é¿å å¿«è¯·æ±æ¶éª¨æ¶éªä¸ä¸ã 739 if (showTransition || articleListNav) { 740 showLoadingState(mainContent, url); 741 } 742 743 // è¯·æ±æ°é¡µé¢ 744 const response = await fetch(url, { 745 signal: abortController.signal, 746 headers: { 747 'X-Requested-With': 'SPA' 748 } 749 }); 750 751 if (!response.ok) { 752 throw new Error(`HTTP ${response.status}`); 753 } 754 755 const html = await response.text(); 756 clearSkeletonTimer(); 757 758 // è§£ææ°é¡µé¢ 759 const parser = new DOMParser(); 760 const doc = parser.parseFromString(html, 'text/html'); 761 762 // è·åæ°å 容 763 const newContent = doc.getElementById(MAIN_CONTENT_ID); 764 const newTitle = doc.querySelector('title'); 765 766 if (newContent) { 767 clearSkeletonTimer(); 768 const swapInPlace = articleListNav && !skeletonShown; 769 const assetsReady = adoptDocumentAssets(doc); 770 771 if (!swapInPlace) { 772 await fadeOut(mainContent, articleListNav); 773 } 774 await assetsReady; 775 776 // æ¿æ¢å 容ãå 叿æå¨ body ä¸çå¼¹çªï¼é¿å æ§é®ç½©ç使°é¡µé¢ã
777 dismissPopupAd(); 778 mainContent.innerHTML = newContent.innerHTML; 779 runEmbeddedScripts(newContent); 780 runNewPageScripts(doc); 781 if (window.CustomSelect && typeof window.CustomSelect.init === 'function') { 782 window.CustomSelect.init(mainContent); 783 } 784 if (mainContent.querySelector('.profile-page') && typeof window.initProfilePage === 'function') { 785 window.initProfilePage(); 786 } 787 788 // æ´æ°æ é¢ 789 if (newTitle) { 790 document.title = newTitle.textContent; 791 } 792 793 // æ´æ°å¯¼èªç¶æ 794 updateActiveNav(url); 795 796 if (searchNavKeepsScroll) { 797 alignListSearch(); 798 } else { 799 window.scrollTo({ top: 0, behavior: 'instant' }); 800 } 801 802 if (swapInPlace) { 803 mainContent.classList.remove('spa-loading'); 804 mainContent.style.minHeight = ''; 805 mainContent.style.opacity = '1'; 806 mainContent.style.transform = 'none'; 807 hideLoadingState(); 808 } else { 809 await fadeIn(mainContent, articleListNav); 810 } 811 812 // éæ°åå§å页é¢èæ¬ 813 reinitializeScripts(); 814 815 // 触åèªå®ä¹äºä»¶ 816 document.dispatchEvent(new CustomEvent('spa:loaded', { detail: { url } })); 817 } else { 818 // æ²¡ææ¾å°ä¸»å 容ï¼åé 819 window.location.href = url; 820 } 821 822 } catch (error) { 823 if (error.name === 'AbortError') { 824 // 请æ±è¢«åæ¶ï¼å¿½ç¥ 825 return; 826 } 827 console.error('SPA navigation failed:', error); 828 // åéå°ä¼ ç»å·æ° 829 window.location.href = url; 830 } finally { 831 clearSkeletonTimer(); 832 searchNavKeepsScroll = false; 833 isNavigating = false; 834 abortController = null; 835 } 836 } 837 838 /** 839 * æ¾ç¤ºå è½½ç¶æï¼éª¨æ¶å± + è¿åº¦æ¡ï¼ 840 */ 841 function isArticleListUrl(url) { 842 const path = new URL(url, window.location.href).pathname.replace(/\/+$/, '').toLowerCase() || '/'; 843 return ARTICLE_LIST_PATHS.indexOf(path) !== -1; 844 } 845 846 function articleGridSkeletonHtml() { 847 const template = document.getElementById('articleGridSkeletonTemplate'); 848 if (!template) return ''; 849 const clone = template.cloneNode(true); 850 clone.querySelectorAll('style').forEach(function(style) { style.remove(); }); 851 return clone.innerHTML.trim(); 852 } 853 854 function clearSkeletonTimer() { 855 if (skeletonTimer) { 856 clearTimeout(skeletonTimer); 857 skeletonTimer = null; 858 } 859 } 860 861 function showLoadingState(container, url) { 862 skeletonShown = false; 863 // æ¾ç¤ºé¡¶é¨è¿åº¦æ¡ 864 const loadingBar = document.getElementById('spaLoadingBar'); 865 if (loadingBar) { 866 loadingBar.classList.remove('complete'); 867 loadingBar.classList.add('loading'); 868 } 869 870 // ä¿ç容å¨é«åº¦ï¼é¿å è·³å¨ 871 container.style.minHeight = container.offsetHeight + 'px'; 872 873 // æ·»å å 载类 874 container.classList.add('spa-loading'); 875 876 if (url && isArticleListUrl(url)) { 877 clearSkeletonTimer(); 878 skeletonShown = false; 879 skeletonTimer = setTimeout(function() { 880 skeletonTimer = null; 881 if (!isNavigating) return; 882 const markup = articleGridSkeletonHtml(); 883 if (!markup) return; 884 skeletonShown = true; 885 container.innerHTML = markup; 886 if (!searchNavKeepsScroll) window.scrollTo({ top: 0, behavior: 'instant' }); 887 }, SKELETON_DELAY_MS); 888 return; 889 } 890 891 // å建骨æ¶å±è¦çå± 892 const skeleton = document.createElement('div'); 893 skeleton.className = 'spa-skeleton-overlay'; 894 skeleton.innerHTML = ` 895 <div class="skeleton-content"> 896 <div class="skeleton-header"></div> 897 <div class="skeleton-cards"> 898 <div class="skeleton-card"></div> 899 <div class="skeleton-card"></div> 900 <div class="skeleton-card"></div> 901 <div class="skeleton-card"></div> 902 <div class="skeleton-card"></div> 903 <div class="skeleton-card"></div> 904 </div> 905 </div> 906 `; 907 container.appendChild(skeleton); 908 909 // 触åéç»åæ¾ç¤º 910 requestAnimationFrame(() => { 911 skeleton.classList.add('show'); 912 }); 913 } 914 915 function runEmbeddedScripts(sourceRoot) { 916 if (!sourceRoot) return; 917 sourceRoot.querySelectorAll('script').forEach(function(oldScript) { 918 if (oldScript.src) return; 919 const type = (oldScript.getAttribute('type') || '').toLowerCase(); 920 if (type && type !== 'text/javascript' && type !== 'application/javascript' && type !== 'module') return; 921 const script = document.createElement('script'); 922 if (type === 'module') script.type = 'module';
923 script.textContent = oldScript.textContent; 924 document.body.appendChild(script); 925 script.remove(); 926 }); 927 } 928 929 /** 930 * éèå è½½ç¶æ 931 * 932 * è¿åº¦æ¡å° 100% ååªå䏿¬¡æ·¡åºï¼ä¸ååå° 0ã 933 * 䏿¬¡å¯¼èªå¼å§æ¶ç± showLoadingState éç½®ç¶æã 934 */ 935 function hideLoadingState() { 936 const loadingBar = document.getElementById('spaLoadingBar'); 937 if (loadingBar) { 938 // ä» loading åæ¢å° completeï¼è®© CSS è´è´£å®½åº¦åéæåº¦å¨ç» 939 loadingBar.classList.remove('loading'); 940 loadingBar.classList.add('complete'); 941 // ä¸åç§»é¤ completeï¼é¿å è§è§ä¸ä» 100% è·³å 0% 942 } 943 } 944 945 /** 946 * æ·¡åºå¨ç» 947 */ 948 function fadeOut(element, lockMotion) { 949 return new Promise(resolve => { 950 element.style.opacity = '0'; 951 element.style.transform = lockMotion ? 'none' : 'translateY(10px)'; 952 setTimeout(resolve, TRANSITION_DURATION / 2); 953 }); 954 } 955 956 /** 957 * æ·¡å ¥å¨ç» 958 */ 959 function fadeIn(element, lockMotion) { 960 return new Promise(resolve => { 961 // éèå è½½è¿åº¦æ¡ 962 hideLoadingState(); 963 964 // ç§»é¤éª¨æ¶å± 965 const skeleton = element.querySelector('.spa-skeleton-overlay'); 966 if (skeleton) { 967 skeleton.remove(); 968 } 969 970 element.classList.remove('spa-loading'); 971 element.style.minHeight = ''; 972 element.style.opacity = '0'; 973 element.style.transform = lockMotion ? 'none' : 'translateY(10px)'; 974 975 // 触åéç» 976 element.offsetHeight; 977 978 element.style.transition = lockMotion 979 ? `opacity ${TRANSITION_DURATION}ms ease` 980 : `opacity ${TRANSITION_DURATION}ms ease, transform ${TRANSITION_DURATION}ms ease`; 981 element.style.opacity = '1'; 982 if (!lockMotion) { 983 element.style.transform = 'translateY(0)'; 984 } 985 986 setTimeout(() => { 987 element.style.transition = ''; 988 element.style.transform = ''; 989 resolve(); 990 }, TRANSITION_DURATION); 991 }); 992 } 993 994 /** 995 * æ´æ°å¯¼èªæ¿æ´»ç¶æ 996 */ 997 function updateActiveNav(url) { 998 const urlObj = new URL(url, window.location.origin); 999 const path = urlObj.pathname; 1000 const filter = urlObj.searchParams.get('filter') || ''; 1001 const fullUrl = path + urlObj.search; 1002 1003 // ç§»é¤ææ active 1004 document.querySelectorAll('.nav-link-icon.active').forEach(link => { 1005 link.classList.remove('active'); 1006 }); 1007 1008 // æ¾å°å¹é ç龿¥å¹¶æ¿æ´» 1009 document.querySelectorAll('.nav-link-icon').forEach(link => { 1010 const href = link.getAttribute('href'); 1011 if (!href) return; 1012 1013 let isActive = false; 1014 1015 // 精确å¹é ï¼å æ¬æ¥è¯¢åæ°ï¼ 1016 if (href === fullUrl || href === path + urlObj.search) { 1017 isActive = true; 1018 } 1019 // é¦é¡µç¹æ®å¤ç 1020 else if (href === '/' && path === '/') { 1021 isActive = true; 1022 } 1023 // Match 页é¢å¸¦ filter åæ°çå¤ç 1024 else if (href.startsWith('/Match?filter=') && path.startsWith('/Match')) { 1025 const hrefFilter = new URL(href, window.location.origin).searchParams.get('filter'); 1026 if (hrefFilter === filter) { 1027 isActive = true; 1028 } 1029 } 1030 // ç§»é¤ç¡¬ç¼ç çè¶³çè·¯ç±æ£æ¥ï¼ä½¿ç¨éç¨å¹é 1031 // League é¡µé¢ 1032 else if (href === '/League' && path.startsWith('/League')) { 1033 isActive = true; 1034 } 1035 // News é¡µé¢ 1036 else if (href === '/News' && path.startsWith('/News')) { 1037 isActive = true; 1038 } 1039 1040 if (isActive) { 1041 link.classList.add('active'); 1042 } 1043 }); 1044 } 1045 1046 /** 1047 * éæ°åå§å页é¢èæ¬ 1048 */ 1049 function dismissPopupAd() { 1050 document.querySelectorAll('#popupAdOverlay').forEach(function (el) { 1051 el.remove(); 1052 }); 1053 document.documentElement.classList.remove('popup-ad-open'); 1054 document.body.classList.remove('popup-ad-open'); 1055 document.body.style.top = ''; 1056 } 1057 1058 function reinitializeScripts() { 1059 // éæ°åºç¨ i18n 1060 if (window.i18n && window.i18n.updatePageTranslations) { 1061 window.i18n.updatePageTranslations(); 1062 } 1063 1064 // éæ°åå§åå¾çæå è½½ 1065 initLazyImages(); 1066 1067 // æ´æ°å½å页é¢ç导èªç¶æ 1068 updateActiveNav(window.location.pathname + window.location.search); 1069 1070 // éæ°åå§åå ¶ä»ç»ä»¶... 1071 if (typeof initMatchCards === 'function') { 1072 initMatchCards(); 1073 } 1074 if (window.CustomSelect && typeof window.CustomSelect.init === 'function') { 1075 window.CustomSelect.init(document.getElementById(MAIN_CONTENT_ID) || document); 1076 } 1077 if (document.querySelector('.match-list-page') && typeof window.initMatchListPage === 'function') { 1078 window.initMatchListPage(); 1079 } 1080 if (document.querySelector('.profile-page') && typeof window.initProfilePage === 'function') { 1081 window.initProfilePage(); 1082 } 1083 } 1084 1085 function assetPath(url, base) { 1086 try { 1087 return new URL(url, base || window.location.href).pathname; 1088 } catch (e) { 1089 return ''; 1090 } 1091 } 1092 1093 function stylesheetAlreadyLoaded(href) { 1094 const path = assetPath(href); 1095 if (!path) return true; 1096 return Array.prototype.some.call(document.querySelectorAll('link[rel="stylesheet"]'), function(el) { 1097 return assetPath(el.href) === path; 1098 }); 1099 } 1100 1101 function waitForStylesheet(el) { 1102 return new Promise(function(resolve) { 1103 let settled = false;
1104 const done = function() { 1105 if (settled) return; 1106 settled = true; 1107 resolve(); 1108 }; 1109 el.addEventListener('load', done, { once: true }); 1110 el.addEventListener('error', done, { once: true }); 1111 setTimeout(done, 2000); 1112 }); 1113 } 1114 1115 /** 1116 * ææ°é¡µé¢ head éçæ ·å¼è¡¨å style åæå°å½åææ¡£ï¼ 1117 * è¿æ · SPA æ¢å ¥ HTML æ¶ç»ä»¶æ ·å¼å·²ç»åå¨ã 1118 */ 1119 function adoptDocumentAssets(doc) { 1120 const pending = []; 1121 if (!stylesheetAlreadyLoaded('/css/match-list.css')) { 1122 const matchCss = document.createElement('link'); 1123 matchCss.rel = 'stylesheet'; 1124 matchCss.href = '/css/match-list.css'; 1125 document.head.appendChild(matchCss); 1126 pending.push(waitForStylesheet(matchCss)); 1127 } 1128 doc.querySelectorAll('link[rel="stylesheet"]').forEach(function(link) { 1129 const href = link.getAttribute('href'); 1130 if (!href || stylesheetAlreadyLoaded(href)) return; 1131 const el = document.createElement('link'); 1132 el.rel = 'stylesheet'; 1133 el.href = new URL(href, window.location.href).href; 1134 if (link.media) el.media = link.media; 1135 document.head.appendChild(el); 1136 pending.push(waitForStylesheet(el)); 1137 }); 1138 1139 const styleTags = []; 1140 if (doc.head) { 1141 doc.head.querySelectorAll('style').forEach(function(style) { styleTags.push(style); }); 1142 } 1143 if (doc.body) { 1144 doc.body.querySelectorAll('style').forEach(function(style) { 1145 if (!style.closest('#' + MAIN_CONTENT_ID)) styleTags.push(style); 1146 }); 1147 } 1148 const existingStyles = Array.prototype.map.call(document.head.querySelectorAll('style'), function(style) { 1149 return (style.textContent || '').trim(); 1150 }); 1151 styleTags.forEach(function(style) { 1152 const text = (style.textContent || '').trim(); 1153 if (!text || existingStyles.indexOf(text) !== -1) return; 1154 existingStyles.push(text); 1155 const el = document.createElement('style'); 1156 el.setAttribute('data-spa-style', '1'); 1157 el.textContent = style.textContent; 1158 document.head.appendChild(el); 1159 }); 1160 1161 return Promise.all(pending); 1162 } 1163 1164 /** 1165 * æ§è¡æ°é¡µé¢éã主å 容ä¹å¤çèæ¬ï¼å¸å± @section Scriptsï¼ã 1166 * å·²ç»å¨å½åææ¡£ä¸çå¸å±èæ¬ä¸é夿§è¡ã 1167 */ 1168 function runNewPageScripts(doc) { 1169 if (!doc.body) return; 1170 const existingInline = new Set(); 1171 const existingSrc = new Set(); 1172 document.querySelectorAll('script').forEach(function(script) { 1173 if (script.src) { 1174 const path = assetPath(script.src); 1175 if (path) existingSrc.add(path); 1176 } else if (script.textContent) { 1177 existingInline.add(script.textContent.trim()); 1178 } 1179 }); 1180 1181 doc.body.querySelectorAll('script').forEach(function(oldScript) { 1182 if (oldScript.closest('#' + MAIN_CONTENT_ID)) return; 1183 const type = (oldScript.getAttribute('type') || '').toLowerCase(); 1184 if (type && type !== 'text/javascript' && type !== 'application/javascript' && type !== 'module') return; 1185 1186 if (oldScript.src || oldScript.getAttribute('src')) { 1187 const src = oldScript.getAttribute('src'); 1188 const path = assetPath(src); 1189 if (!src || !path || existingSrc.has(path)) return; 1190 existingSrc.add(path); 1191 const script = document.createElement('script'); 1192 script.src = new URL(src, window.location.href).href; 1193 if (oldScript.async) script.async = true; 1194 document.body.appendChild(script); 1195 return; 1196 } 1197 1198 const text = (oldScript.textContent || '').trim(); 1199 if (!text || existingInline.has(text)) return; 1200 existingInline.add(text); 1201 const script = document.createElement('script'); 1202 script.textContent = oldScript.textContent; 1203 document.body.appendChild(script); 1204 }); 1205 } 1206 1207 /** 1208 * åå§åå¾çæå è½½ 1209 */ 1210 function initLazyImages() { 1211 const images = document.querySelectorAll('img[data-src]'); 1212 1213 if ('IntersectionObserver' in window) { 1214 const observer = new IntersectionObserver((entries) => {
1215 entries.forEach(entry => { 1216 if (entry.isIntersecting) { 1217 const img = entry.target; 1218 img.src = img.dataset.src; 1219 img.removeAttribute('data-src'); 1220 observer.unobserve(img); 1221 } 1222 }); 1223 }, { 1224 rootMargin: '100px' 1225 }); 1226 1227 images.forEach(img => observer.observe(img)); 1228 } else { 1229 // åéæ¹æ¡ 1230 images.forEach(img => { 1231 img.src = img.dataset.src; 1232 img.removeAttribute('data-src'); 1233 }); 1234 } 1235 } 1236 1237 // æ´é²ç»å ¨å± 1238 window.spaRouter = { 1239 init, 1240 navigateTo, 1241 openMatchDetail: showMatchDetailSkeletonAndGo, 1242 refresh: () => loadPage(window.location.pathname + window.location.search) 1243 }; 1244 1245 // DOM å è½½å®æååå§å 1246 if (document.readyState === 'loading') { 1247 document.addEventListener('DOMContentLoaded', init); 1248 } else { 1249 init(); 1250 } 1251 1252})();
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.