fix(webpub): the published page could not sort a date, and could not be unsorted
parseFloat accepts a numeric PREFIX. "2026-08-10" therefore became the number 2026, every date in the same year compared equal, and since the sort is stable nothing moved: the Date column looked as though it were simply not sortable. The same trap caught every callsign starting with a digit - 8B81SU and 8P9AB both read as 8 - and the times, where "09:56" became 9. The numeric test is now anchored to the whole cell, so anything that is not entirely a number is compared as text, which is exactly right for an ISO date. Sorting had no way back either. Each row now carries the index it was published at, and a third click on a header restores that order. Headers also show an arrow: with no indicator, a column that silently refused to sort was indistinguishable from one that had sorted into the same order. Blank cells sink in both directions rather than leading the ascending sort. An empty field is missing data, not the smallest value. Tested where it can be: the page ships its own script, so a regression is silent - the table still renders, it just sorts wrongly.
This commit is contained in:
+32
-12
@@ -233,6 +233,9 @@ th,td{padding:.45rem .6rem;text-align:left;border-bottom:1px solid var(--line);w
|
||||
th{position:sticky;top:0;background:var(--head);font-size:.72rem;letter-spacing:.05em;
|
||||
text-transform:uppercase;color:var(--mut);cursor:pointer;user-select:none}
|
||||
th:hover{color:var(--fg)}
|
||||
th::after{content:'';font-size:.7em;opacity:.7}
|
||||
th[data-asc="1"]::after{content:' \25B2'}
|
||||
th[data-asc="0"]::after{content:' \25BC'}
|
||||
tbody tr:nth-child(even){background:var(--zebra)}
|
||||
tbody tr:last-child td{border-bottom:0}
|
||||
td.call{font-family:ui-monospace,Consolas,monospace;font-weight:700;color:var(--accent)}
|
||||
@@ -248,7 +251,9 @@ td.call{font-family:ui-monospace,Consolas,monospace;font-weight:700;color:var(--
|
||||
}
|
||||
b.WriteString(`</tr></thead><tbody>`)
|
||||
for i := range qsos {
|
||||
b.WriteString(`<tr>`)
|
||||
// The row's position as published. Sorting is a view on top of it, so a
|
||||
// third click can put the table back the way the operator first saw it.
|
||||
b.WriteString(`<tr data-i="` + strconv.Itoa(i) + `">`)
|
||||
for _, c := range cols {
|
||||
cls := ""
|
||||
if c.Key == "callsign" {
|
||||
@@ -262,23 +267,38 @@ td.call{font-family:ui-monospace,Consolas,monospace;font-weight:700;color:var(--
|
||||
<p class="foot">Generated by OpsLog</p>
|
||||
</div>
|
||||
<script>
|
||||
// Click a header to sort. Kept tiny and dependency-free: the page has to work
|
||||
// offline and on any hosting.
|
||||
// Click a header to sort: ascending, descending, then back to the published
|
||||
// order. Kept tiny and dependency-free — the page has to work offline and on any
|
||||
// hosting, so no library is loaded.
|
||||
//
|
||||
// NUM is deliberately strict: the whole cell must be a number. parseFloat alone
|
||||
// accepts a numeric PREFIX, so "2026-08-10" became 2026 and every date in a year
|
||||
// compared equal — the date column looked as though it simply would not sort.
|
||||
// Callsigns starting with a digit (8B81SU) hit the same trap.
|
||||
var NUM=/^[+-]?\d+(\.\d+)?$/;
|
||||
document.querySelectorAll('th').forEach(function(th,i){
|
||||
th.addEventListener('click',function(){
|
||||
var tb=th.closest('table').tBodies[0],
|
||||
rows=Array.prototype.slice.call(tb.rows),
|
||||
asc=th.dataset.asc!=='1';
|
||||
rows.sort(function(a,b){
|
||||
var x=a.cells[i].textContent.trim(), y=b.cells[i].textContent.trim(),
|
||||
nx=parseFloat(x), ny=parseFloat(y),
|
||||
n=!isNaN(nx)&&!isNaN(ny)&&x!==''&&y!=='';
|
||||
var c=n?(nx-ny):x.localeCompare(y);
|
||||
return asc?c:-c;
|
||||
});
|
||||
cur=th.dataset.asc,
|
||||
next=cur===undefined?'1':(cur==='1'?'0':'');
|
||||
if(next===''){
|
||||
// Third click: restore the order the page was published in.
|
||||
rows.sort(function(a,b){return a.dataset.i-b.dataset.i});
|
||||
}else{
|
||||
var asc=next==='1';
|
||||
rows.sort(function(a,b){
|
||||
var x=a.cells[i].textContent.trim(), y=b.cells[i].textContent.trim();
|
||||
// Blanks always sink, whichever way the column is pointing: an empty
|
||||
// cell is missing data, not the smallest value.
|
||||
if(x===''||y==='') return x===y?0:(x===''?1:-1);
|
||||
var c=NUM.test(x)&&NUM.test(y)?(parseFloat(x)-parseFloat(y)):x.localeCompare(y);
|
||||
return asc?c:-c;
|
||||
});
|
||||
}
|
||||
rows.forEach(function(r){tb.appendChild(r)});
|
||||
th.closest('tr').querySelectorAll('th').forEach(function(o){delete o.dataset.asc});
|
||||
th.dataset.asc=asc?'1':'0';
|
||||
if(next!=='') th.dataset.asc=next;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user