|
15 | 15 | initSearch(); |
16 | 16 | initSorting(); |
17 | 17 | initToggleButtons(); |
| 18 | + initCoverageNav(); |
18 | 19 | initTreeControls(); |
19 | 20 | initViewToggle(); |
| 21 | + initSettingsDropdown(); |
20 | 22 | initTlaNavigation(); |
21 | 23 | initLineHighlight(); |
22 | 24 | initColumnToggles(); |
23 | 25 | initPopupResize(); |
| 26 | + initFileNavTooltips(); |
| 27 | + initFileNavKeys(); |
24 | 28 |
|
25 | 29 | // Reveal page now that all init is done |
26 | 30 | document.documentElement.classList.remove('no-transitions'); |
|
994 | 998 | const buttons = document.querySelectorAll('.button_toggle_coveredLine, .button_toggle_uncoveredLine, .button_toggle_partialCoveredLine, .button_toggle_excludedLine'); |
995 | 999 |
|
996 | 1000 | buttons.forEach(function(button) { |
| 1001 | + var lineClass = button.value; |
| 1002 | + if (!document.querySelector('.' + lineClass)) { |
| 1003 | + button.disabled = true; |
| 1004 | + button.classList.remove('show_' + lineClass); |
| 1005 | + return; |
| 1006 | + } |
997 | 1007 | button.addEventListener('click', function() { |
998 | 1008 | const lineClass = this.value; |
999 | 1009 | const showClass = 'show_' + lineClass; |
|
1006 | 1016 | lines.forEach(function(line) { |
1007 | 1017 | line.classList.toggle(showClass); |
1008 | 1018 | }); |
| 1019 | + document.dispatchEvent(new CustomEvent('coverage-toggled')); |
1009 | 1020 | }); |
1010 | 1021 | }); |
1011 | 1022 |
|
|
1023 | 1034 | lines.forEach(function(line) { |
1024 | 1035 | line.classList.toggle(showClass); |
1025 | 1036 | }); |
| 1037 | + document.dispatchEvent(new CustomEvent('coverage-toggled')); |
| 1038 | + }); |
| 1039 | + }); |
| 1040 | + } |
| 1041 | + |
| 1042 | + // =========================================== |
| 1043 | + // Coverage Navigation (prev/next uncovered) |
| 1044 | + // =========================================== |
| 1045 | + |
| 1046 | + function initCoverageNav() { |
| 1047 | + var prevBtn = document.getElementById('nav-prev'); |
| 1048 | + var nextBtn = document.getElementById('nav-next'); |
| 1049 | + var counter = document.getElementById('nav-counter'); |
| 1050 | + |
| 1051 | + if (!prevBtn || !nextBtn || !counter) return; |
| 1052 | + |
| 1053 | + var gapLines = []; |
| 1054 | + var currentIndex = -1; |
| 1055 | + |
| 1056 | + function collectGapLines() { |
| 1057 | + var uncovered = document.querySelectorAll('tr.uncoveredLine.show_uncoveredLine'); |
| 1058 | + var partial = document.querySelectorAll('tr.partialCoveredLine.show_partialCoveredLine'); |
| 1059 | + var merged = []; |
| 1060 | + var i; |
| 1061 | + for (i = 0; i < uncovered.length; i++) merged.push(uncovered[i]); |
| 1062 | + for (i = 0; i < partial.length; i++) merged.push(partial[i]); |
| 1063 | + // Sort by DOM order |
| 1064 | + merged.sort(function(a, b) { |
| 1065 | + var pos = a.compareDocumentPosition(b); |
| 1066 | + if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; |
| 1067 | + if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; |
| 1068 | + return 0; |
1026 | 1069 | }); |
| 1070 | + gapLines = merged; |
| 1071 | + currentIndex = -1; |
| 1072 | + updateCounter(); |
| 1073 | + } |
| 1074 | + |
| 1075 | + function updateCounter() { |
| 1076 | + if (gapLines.length === 0) { |
| 1077 | + counter.textContent = 'All lines covered'; |
| 1078 | + prevBtn.disabled = true; |
| 1079 | + nextBtn.disabled = true; |
| 1080 | + } else { |
| 1081 | + var display = currentIndex >= 0 ? (currentIndex + 1) : 0; |
| 1082 | + counter.textContent = display + ' / ' + gapLines.length; |
| 1083 | + prevBtn.disabled = false; |
| 1084 | + nextBtn.disabled = false; |
| 1085 | + } |
| 1086 | + } |
| 1087 | + |
| 1088 | + function navigateTo(index) { |
| 1089 | + if (gapLines.length === 0) return; |
| 1090 | + // Remove previous highlight |
| 1091 | + var prev = document.querySelector('tr.source-line.nav-highlight'); |
| 1092 | + if (prev) prev.classList.remove('nav-highlight'); |
| 1093 | + |
| 1094 | + currentIndex = index; |
| 1095 | + var row = gapLines[currentIndex]; |
| 1096 | + row.scrollIntoView({ block: 'center', behavior: 'instant' }); |
| 1097 | + row.classList.add('nav-highlight'); |
| 1098 | + setTimeout(function() { |
| 1099 | + row.classList.remove('nav-highlight'); |
| 1100 | + }, 1500); |
| 1101 | + updateCounter(); |
| 1102 | + } |
| 1103 | + |
| 1104 | + function nextGap() { |
| 1105 | + if (gapLines.length === 0) return; |
| 1106 | + var next = currentIndex + 1; |
| 1107 | + if (next >= gapLines.length) next = 0; |
| 1108 | + navigateTo(next); |
| 1109 | + } |
| 1110 | + |
| 1111 | + function prevGap() { |
| 1112 | + if (gapLines.length === 0) return; |
| 1113 | + var prev = currentIndex - 1; |
| 1114 | + if (prev < 0) prev = gapLines.length - 1; |
| 1115 | + navigateTo(prev); |
| 1116 | + } |
| 1117 | + |
| 1118 | + prevBtn.addEventListener('click', prevGap); |
| 1119 | + nextBtn.addEventListener('click', nextGap); |
| 1120 | + |
| 1121 | + document.addEventListener('keydown', function(e) { |
| 1122 | + var tag = (e.target.tagName || '').toLowerCase(); |
| 1123 | + if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return; |
| 1124 | + if (e.key === 'n') nextGap(); |
| 1125 | + if (e.key === 'p') prevGap(); |
| 1126 | + }); |
| 1127 | + |
| 1128 | + document.addEventListener('coverage-toggled', function() { |
| 1129 | + collectGapLines(); |
1027 | 1130 | }); |
| 1131 | + |
| 1132 | + collectGapLines(); |
1028 | 1133 | } |
1029 | 1134 |
|
1030 | 1135 | // =========================================== |
|
1274 | 1379 | } |
1275 | 1380 | } |
1276 | 1381 |
|
| 1382 | + // =========================================== |
| 1383 | + // Settings Dropdown (mobile gear icon) |
| 1384 | + // =========================================== |
| 1385 | + |
| 1386 | + function initSettingsDropdown() { |
| 1387 | + var btn = document.getElementById('settings-btn'); |
| 1388 | + var dropdown = document.getElementById('settings-dropdown'); |
| 1389 | + var header = document.querySelector('.main-header'); |
| 1390 | + if (!btn || !dropdown || !header) return; |
| 1391 | + |
| 1392 | + var viewToggle = document.getElementById('view-toggle'); |
| 1393 | + var themeToggle = document.getElementById('theme-toggle'); |
| 1394 | + var isMobile = false; |
| 1395 | + |
| 1396 | + // Reference node: settings-btn, so we can insert before it when moving back |
| 1397 | + function moveToDropdown() { |
| 1398 | + if (viewToggle && viewToggle.parentNode !== dropdown) { |
| 1399 | + dropdown.appendChild(viewToggle); |
| 1400 | + } |
| 1401 | + if (themeToggle && themeToggle.parentNode !== dropdown) { |
| 1402 | + dropdown.appendChild(themeToggle); |
| 1403 | + } |
| 1404 | + } |
| 1405 | + |
| 1406 | + function moveToHeader() { |
| 1407 | + // Insert before settings-btn so they appear in original order |
| 1408 | + if (viewToggle && viewToggle.parentNode !== header) { |
| 1409 | + header.insertBefore(viewToggle, btn); |
| 1410 | + } |
| 1411 | + if (themeToggle && themeToggle.parentNode !== header) { |
| 1412 | + header.insertBefore(themeToggle, btn); |
| 1413 | + } |
| 1414 | + } |
| 1415 | + |
| 1416 | + function checkBreakpoint() { |
| 1417 | + var nowMobile = window.innerWidth <= 1024; |
| 1418 | + if (nowMobile === isMobile) return; |
| 1419 | + isMobile = nowMobile; |
| 1420 | + if (isMobile) { |
| 1421 | + moveToDropdown(); |
| 1422 | + } else { |
| 1423 | + dropdown.classList.remove('open'); |
| 1424 | + moveToHeader(); |
| 1425 | + } |
| 1426 | + } |
| 1427 | + |
| 1428 | + // Toggle dropdown on button click |
| 1429 | + btn.addEventListener('click', function(e) { |
| 1430 | + e.stopPropagation(); |
| 1431 | + dropdown.classList.toggle('open'); |
| 1432 | + }); |
| 1433 | + |
| 1434 | + // Close on outside click |
| 1435 | + document.addEventListener('click', function(e) { |
| 1436 | + if (!dropdown.contains(e.target) && e.target !== btn) { |
| 1437 | + dropdown.classList.remove('open'); |
| 1438 | + } |
| 1439 | + }); |
| 1440 | + |
| 1441 | + // Close on Escape |
| 1442 | + document.addEventListener('keydown', function(e) { |
| 1443 | + if (e.key === 'Escape') { |
| 1444 | + dropdown.classList.remove('open'); |
| 1445 | + } |
| 1446 | + }); |
| 1447 | + |
| 1448 | + // Respond to resize |
| 1449 | + window.addEventListener('resize', checkBreakpoint); |
| 1450 | + |
| 1451 | + // Initial check |
| 1452 | + checkBreakpoint(); |
| 1453 | + } |
| 1454 | + |
1277 | 1455 | // =========================================== |
1278 | 1456 | // Popup Resize (only when overflowing) |
1279 | 1457 | // =========================================== |
|
1331 | 1509 | var idx = fileLinks.indexOf(currentPage); |
1332 | 1510 | if (idx === -1) return; |
1333 | 1511 |
|
1334 | | - var prev = idx > 0 ? fileLinks[idx - 1] : 'index.html'; |
1335 | | - var next = idx < fileLinks.length - 1 ? fileLinks[idx + 1] : 'index.html'; |
| 1512 | + var prev = idx > 0 ? fileLinks[idx - 1] : null; |
| 1513 | + var next = idx < fileLinks.length - 1 ? fileLinks[idx + 1] : null; |
1336 | 1514 |
|
1337 | | - navPrevs.forEach(function(el) { el.setAttribute('href', prev); }); |
1338 | | - navNexts.forEach(function(el) { el.setAttribute('href', next); }); |
| 1515 | + function updateNavLinks(els, href) { |
| 1516 | + for (var i = 0; i < els.length; i++) { |
| 1517 | + var el = els[i]; |
| 1518 | + if (href) { |
| 1519 | + // Enable: ensure it's an <a> with the correct href |
| 1520 | + if (el.tagName === 'A') { |
| 1521 | + el.setAttribute('href', href); |
| 1522 | + } else { |
| 1523 | + // Replace disabled <span> with an <a> |
| 1524 | + var a = document.createElement('a'); |
| 1525 | + a.className = el.className.replace(/\bdisabled\b/, '').trim(); |
| 1526 | + a.href = href; |
| 1527 | + a.title = el.title; |
| 1528 | + a.innerHTML = el.innerHTML; |
| 1529 | + el.parentNode.replaceChild(a, el); |
| 1530 | + } |
| 1531 | + } else { |
| 1532 | + // Disable: ensure it's a <span> with disabled class |
| 1533 | + if (el.tagName === 'A') { |
| 1534 | + var span = document.createElement('span'); |
| 1535 | + span.className = el.className + ' disabled'; |
| 1536 | + span.title = el.title; |
| 1537 | + span.innerHTML = el.innerHTML; |
| 1538 | + el.parentNode.replaceChild(span, el); |
| 1539 | + } else { |
| 1540 | + el.classList.add('disabled'); |
| 1541 | + } |
| 1542 | + } |
| 1543 | + } |
| 1544 | + } |
| 1545 | + |
| 1546 | + updateNavLinks(navPrevs, prev); |
| 1547 | + updateNavLinks(navNexts, next); |
1339 | 1548 | } |
1340 | 1549 |
|
1341 | 1550 | // =========================================== |
|
1550 | 1759 | }); |
1551 | 1760 | } |
1552 | 1761 |
|
| 1762 | + // =========================================== |
| 1763 | + // File nav keyboard shortcuts ([ and ]) |
| 1764 | + // =========================================== |
| 1765 | + |
| 1766 | + function initFileNavKeys() { |
| 1767 | + var prevLink = document.querySelector('.source-nav-links .nav-prev') || document.querySelector('.nav-links .nav-prev'); |
| 1768 | + var nextLink = document.querySelector('.source-nav-links .nav-next') || document.querySelector('.nav-links .nav-next'); |
| 1769 | + if (!prevLink && !nextLink) return; |
| 1770 | + |
| 1771 | + document.addEventListener('keydown', function(e) { |
| 1772 | + var tag = (e.target.tagName || '').toLowerCase(); |
| 1773 | + if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return; |
| 1774 | + // Re-query to pick up any DOM replacements by initNavOverride |
| 1775 | + var prev = document.querySelector('.source-nav-links a.nav-prev') || document.querySelector('.nav-links a.nav-prev'); |
| 1776 | + var next = document.querySelector('.source-nav-links a.nav-next') || document.querySelector('.nav-links a.nav-next'); |
| 1777 | + if (e.key === '[' && prev) { |
| 1778 | + window.location.href = prev.href; |
| 1779 | + } |
| 1780 | + if (e.key === ']' && next) { |
| 1781 | + window.location.href = next.href; |
| 1782 | + } |
| 1783 | + }); |
| 1784 | + } |
| 1785 | + |
| 1786 | + // =========================================== |
| 1787 | + // Enrich file nav tooltips with actual filenames |
| 1788 | + // =========================================== |
| 1789 | + |
| 1790 | + function initFileNavTooltips() { |
| 1791 | + if (!window.GCOVR_TREE_DATA) return; |
| 1792 | + var links = document.querySelectorAll('.source-nav-links .nav-prev, .source-nav-links .nav-next, .nav-links .nav-prev, .nav-links .nav-next'); |
| 1793 | + for (var i = 0; i < links.length; i++) { |
| 1794 | + var anchor = links[i]; |
| 1795 | + var href = anchor.getAttribute('href'); |
| 1796 | + if (!href || href === '#') continue; |
| 1797 | + var filename = href.replace(/^.*\//, '').replace(/#.*$/, ''); |
| 1798 | + var node = findNodeInTree(window.GCOVR_TREE_DATA, filename); |
| 1799 | + if (node && node.name) { |
| 1800 | + var direction = anchor.classList.contains('nav-prev') ? 'Previous' : 'Next'; |
| 1801 | + anchor.title = direction + ': ' + node.name; |
| 1802 | + } |
| 1803 | + } |
| 1804 | + } |
| 1805 | + |
| 1806 | + function findNodeInTree(nodes, targetLink) { |
| 1807 | + for (var i = 0; i < nodes.length; i++) { |
| 1808 | + var node = nodes[i]; |
| 1809 | + if (node.link === targetLink) return node; |
| 1810 | + if (node.children) { |
| 1811 | + var found = findNodeInTree(node.children, targetLink); |
| 1812 | + if (found) return found; |
| 1813 | + } |
| 1814 | + } |
| 1815 | + return null; |
| 1816 | + } |
| 1817 | + |
1553 | 1818 | // =========================================== |
1554 | 1819 | // Prefetch pages on hover for instant nav |
1555 | 1820 | // =========================================== |
|
0 commit comments