Skip to content

Commit 2b30faf

Browse files
committed
Polish function list UI
1 parent c6a2a2a commit 2b30faf

3 files changed

Lines changed: 212 additions & 71 deletions

File tree

gcovr-templates/html/gcovr.js

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
initPopupResize();
2727
initFileNavTooltips();
2828
initFileNavKeys();
29+
initFunctionListPersistence();
2930

3031
// Reveal page now that all init is done
3132
document.documentElement.classList.remove('no-transitions');
@@ -1156,51 +1157,69 @@
11561157
// ===========================================
11571158

11581159
function initSorting() {
1159-
const headers = document.querySelectorAll('.file-list-header .sortable, .functions-header .sortable');
1160+
var headerSets = [
1161+
{
1162+
selector: '.file-list-header .sortable, .functions-header .sortable',
1163+
getContainer: function() {
1164+
return document.getElementById('file-list') || document.querySelector('.functions-body');
1165+
},
1166+
defaultSort: { key: 'filename', ascending: true }
1167+
},
1168+
{
1169+
selector: '.source-function-header .sortable',
1170+
getContainer: function() {
1171+
return document.querySelector('.source-functions-list');
1172+
},
1173+
defaultSort: null
1174+
}
1175+
];
11601176

1161-
headers.forEach(function(header) {
1162-
header.addEventListener('click', function() {
1163-
const sortKey = this.dataset.sort;
1164-
const isAscending = this.classList.contains('sorted-ascending');
1177+
headerSets.forEach(function(set) {
1178+
var headers = document.querySelectorAll(set.selector);
1179+
if (!headers.length) return;
11651180

1166-
// Remove sorted class from all headers
1167-
headers.forEach(function(h) {
1168-
h.classList.remove('sorted-ascending', 'sorted-descending');
1169-
});
1181+
headers.forEach(function(header) {
1182+
header.addEventListener('click', function() {
1183+
var sortKey = this.dataset.sort;
1184+
var isAscending = this.classList.contains('sorted-ascending');
11701185

1171-
// Toggle sort direction
1172-
this.classList.add(isAscending ? 'sorted-descending' : 'sorted-ascending');
1186+
headers.forEach(function(h) {
1187+
h.classList.remove('sorted-ascending', 'sorted-descending');
1188+
});
1189+
1190+
this.classList.add(isAscending ? 'sorted-descending' : 'sorted-ascending');
11731191

1174-
// Sort the list
1175-
sortList(sortKey, !isAscending);
1192+
sortList(set.getContainer(), sortKey, !isAscending);
1193+
});
11761194
});
1177-
});
11781195

1179-
// Initial sort: directories first, then by filename
1180-
sortList('filename', true);
1196+
if (set.defaultSort) {
1197+
sortList(set.getContainer(), set.defaultSort.key, set.defaultSort.ascending);
1198+
}
1199+
});
11811200
}
11821201

1183-
function sortList(key, ascending) {
1184-
const container = document.getElementById('file-list') || document.querySelector('.functions-body');
1202+
function sortList(container, key, ascending) {
11851203
if (!container) return;
11861204
// Virtual scroll handles its own sorting
11871205
if (container.dataset.virtualScroll) return;
11881206

1189-
const rows = Array.from(container.children);
1207+
var headerEl = container.querySelector('.source-function-header, .file-list-header, .functions-header');
1208+
var rows = Array.from(container.children).filter(function(el) { return el !== headerEl; });
11901209

11911210
rows.sort(function(a, b) {
11921211
// Directories always come first
1193-
const aIsDir = a.classList.contains('directory');
1194-
const bIsDir = b.classList.contains('directory');
1212+
var aIsDir = a.classList.contains('directory');
1213+
var bIsDir = b.classList.contains('directory');
11951214
if (aIsDir && !bIsDir) return -1;
11961215
if (!aIsDir && bIsDir) return 1;
11971216

1198-
let aVal = a.dataset[key] || a.querySelector('[data-sort]')?.dataset.sort || '';
1199-
let bVal = b.dataset[key] || b.querySelector('[data-sort]')?.dataset.sort || '';
1217+
var aVal = a.dataset[key] || a.querySelector('[data-sort]')?.dataset.sort || '';
1218+
var bVal = b.dataset[key] || b.querySelector('[data-sort]')?.dataset.sort || '';
12001219

12011220
// Try to parse as numbers
1202-
const aNum = parseFloat(aVal);
1203-
const bNum = parseFloat(bVal);
1221+
var aNum = parseFloat(aVal);
1222+
var bNum = parseFloat(bVal);
12041223

12051224
if (!isNaN(aNum) && !isNaN(bNum)) {
12061225
return ascending ? aNum - bNum : bNum - aNum;
@@ -1633,7 +1652,7 @@
16331652
localStorage.setItem('gcovr-view-mode', 'nested');
16341653

16351654
// Re-run sorting to maintain state
1636-
sortList('filename', true);
1655+
sortList(document.getElementById('file-list') || document.querySelector('.functions-body'), 'filename', true);
16371656
}
16381657

16391658
buttons.forEach(function(btn) {
@@ -1922,7 +1941,7 @@
19221941
if (scrollBox && row) {
19231942
var thead = scrollBox.querySelector('thead');
19241943
var theadHeight = thead ? thead.offsetHeight : 0;
1925-
scrollBox.scrollTo({ top: row.offsetTop - theadHeight - 8, behavior: 'smooth' });
1944+
scrollBox.scrollTo({ top: row.offsetTop - theadHeight - 8, behavior: 'instant' });
19261945
}
19271946
history.replaceState(null, '', this.getAttribute('href'));
19281947
// Highlight the target row (clear any previous highlight first)
@@ -1940,13 +1959,20 @@
19401959
// ===========================================
19411960

19421961
function initLineHighlight() {
1962+
var clickedFnItem = null;
1963+
19431964
function highlightFromHash(scroll) {
19441965
var prev = document.querySelector('.highlight-target');
19451966
if (prev) prev.classList.remove('highlight-target');
1967+
var prevFn = document.querySelector('.source-function-item.selected');
1968+
if (prevFn) prevFn.classList.remove('selected');
19461969
var id = window.location.hash.slice(1);
19471970
if (!id) return;
19481971
var el = document.getElementById(id);
19491972
if (!el) return;
1973+
var fnItem = clickedFnItem || document.querySelector('.source-function-item[href="#' + id + '"]');
1974+
clickedFnItem = null;
1975+
if (fnItem) fnItem.classList.add('selected');
19501976
var row = el.closest('tr');
19511977
if (row) {
19521978
row.classList.add('highlight-target');
@@ -1955,14 +1981,28 @@
19551981
if (scrollBox) {
19561982
var thead = scrollBox.querySelector('thead');
19571983
var theadHeight = thead ? thead.offsetHeight : 0;
1958-
scrollBox.scrollTo({ top: row.offsetTop - theadHeight - 8, behavior: 'smooth' });
1984+
scrollBox.scrollTo({ top: row.offsetTop - theadHeight - 8, behavior: 'instant' });
19591985
} else {
19601986
row.scrollIntoView({ block: 'center' });
19611987
}
19621988
}
19631989
}
19641990
}
19651991

1992+
// Handle clicks on function list items directly
1993+
var fnList = document.querySelector('.source-functions-list');
1994+
if (fnList) {
1995+
fnList.addEventListener('click', function(e) {
1996+
var item = e.target.closest('.source-function-item');
1997+
if (!item) return;
1998+
e.preventDefault();
1999+
clickedFnItem = item;
2000+
var href = item.getAttribute('href');
2001+
if (href) history.replaceState(null, '', href);
2002+
highlightFromHash(true);
2003+
});
2004+
}
2005+
19662006
// Event delegation: single listener on the table container
19672007
var container = document.querySelector('.source-table-container');
19682008
if (container) {
@@ -2119,4 +2159,18 @@
21192159
});
21202160
}
21212161

2162+
function initFunctionListPersistence() {
2163+
var details = document.querySelector('details.source-functions');
2164+
if (!details) return;
2165+
2166+
var key = 'gcovr-fn-list-open';
2167+
if (sessionStorage.getItem(key) === 'true') {
2168+
details.setAttribute('open', '');
2169+
}
2170+
2171+
details.addEventListener('toggle', function() {
2172+
sessionStorage.setItem(key, details.open ? 'true' : 'false');
2173+
});
2174+
}
2175+
21222176
})();

gcovr-templates/html/source_page.content.html

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,30 +85,34 @@
8585
Functions ({{ function_list | length }})
8686
</summary>
8787
<div class="source-functions-list">
88+
{% set sortable = " sortable" if function_list | length > 1 else "" %}
8889
<div class="source-function-header">
89-
<span class="source-function-col-name">Function</span>
90-
<span class="source-function-col-stat">Calls</span>
91-
<span class="source-function-col-stat">Lines</span>
92-
<span class="source-function-col-stat">Branches</span>
93-
<span class="source-function-col-stat">Blocks</span>
90+
<span class="source-function-col-name{{ sortable }}{% if sortable %} sorted-ascending{% endif %}" data-sort="name">Function</span>
91+
<span class="source-function-col-stat{{ sortable }}" data-sort="calls">Calls</span>
92+
<span class="source-function-col-stat{{ sortable }}" data-sort="lines">Lines</span>
93+
<span class="source-function-col-stat{{ sortable }}" data-sort="branches">Branches</span>
94+
<span class="source-function-col-stat{{ sortable }}" data-sort="blocks">Blocks</span>
9495
</div>
9596
{% for entry in function_list | sort(attribute='line') %}
96-
<a href="#{{ anchor_prefix }}l{{ entry.line }}" class="source-function-item{% if entry.excluded %} fn-excluded{% elif entry.count is defined and entry.count is not none and entry.count > 0 %}{% else %} fn-uncovered{% endif %}">
97+
{% set calls = entry.count if entry.count is defined else entry.execution_count if entry.execution_count is defined else none %}
98+
{% set blks = entry.blocks if entry.blocks is defined else entry.blocks_percent if entry.blocks_percent is defined else none %}
99+
<a href="#{{ anchor_prefix }}l{{ entry.line }}" class="source-function-item{% if entry.excluded %} fn-excluded{% elif calls is not none and calls > 0 %}{% else %} fn-uncovered{% endif %}"
100+
data-name="{{ entry.name }}" data-calls="{{ calls if calls is not none else -1 }}" data-lines="{{ entry.line_coverage if entry.line_coverage is defined else -1 }}" data-branches="{{ entry.branch_coverage if entry.branch_coverage is defined and entry.branch_coverage != '-' else -1 }}" data-blocks="{{ blks if blks is not none else -1 }}">
97101
<span class="source-function-col-name">
98102
<span class="source-function-name">{{ entry.name }}</span>
99103
<span class="source-function-line">:{{ entry.line }}</span>
100104
</span>
101-
<span class="source-function-col-stat{% if entry.excluded %} excluded{% elif not (entry.count is defined and entry.count is not none and entry.count > 0) %} not-called{% endif %}">
102-
{%- if entry.excluded %}&ndash;{% elif entry.count is defined and entry.count is not none and entry.count > 0 %}{{ entry.count }}x{% else %}0{% endif -%}
105+
<span class="source-function-col-stat{% if entry.excluded %} excluded{% elif not (calls is not none and calls > 0) %} not-called{% endif %}">
106+
{%- if entry.excluded %}&ndash;{% elif calls is not none and calls > 0 %}{{ calls }}x{% else %}0{% endif -%}
103107
</span>
104108
<span class="source-function-col-stat{% if entry.excluded %} excluded{% elif entry.line_coverage is defined and entry.line_coverage == 100.0 %} cov-high{% elif entry.line_coverage is defined and entry.line_coverage > 0 %} cov-med{% else %} cov-low{% endif %}">
105109
{%- if entry.excluded %}&ndash;{% elif entry.line_coverage is defined %}{{ entry.line_coverage | round(1) }}%{% else %}&ndash;{% endif -%}
106110
</span>
107111
<span class="source-function-col-stat{% if entry.excluded %} excluded{% elif entry.branch_coverage is defined and entry.branch_coverage != '-' %}{% if entry.branch_coverage == 100.0 %} cov-high{% elif entry.branch_coverage > 0 %} cov-med{% else %} cov-low{% endif %}{% endif %}">
108112
{%- if entry.excluded %}&ndash;{% elif entry.branch_coverage is defined and entry.branch_coverage != '-' %}{{ entry.branch_coverage | round(1) }}%{% else %}&ndash;{% endif -%}
109113
</span>
110-
<span class="source-function-col-stat{% if entry.excluded %} excluded{% elif entry.blocks is defined and entry.blocks is not none %}{% if entry.blocks == 100.0 %} cov-high{% elif entry.blocks > 0 %} cov-med{% else %} cov-low{% endif %}{% endif %}">
111-
{%- if entry.excluded %}&ndash;{% elif entry.blocks is defined and entry.blocks is not none %}{{ entry.blocks | round(1) }}%{% else %}&ndash;{% endif -%}
114+
<span class="source-function-col-stat{% if entry.excluded %} excluded{% elif blks is not none %}{% if blks == 100.0 %} cov-high{% elif blks > 0 %} cov-med{% else %} cov-low{% endif %}{% endif %}">
115+
{%- if entry.excluded %}&ndash;{% elif blks is not none %}{{ blks | round(1) }}%{% else %}&ndash;{% endif -%}
112116
</span>
113117
</a>
114118
{% endfor %}

0 commit comments

Comments
 (0)