diff --git a/assets/css/main.css b/assets/css/main.css index d1780d39..5273e8d7 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -72,14 +72,21 @@ small, .meta, .note, .crumbs { color: var(--muted); } table { border-collapse: collapse; } .specs, .list { width: 100%; clear: both; margin: 16px 0; background: var(--panel); } -.specs th, .specs td, .list td { padding: 6px 10px; border: 1px solid var(--line); text-align: left; vertical-align: top; } +.specs th, .specs td, .list th, .list td { padding: 6px 10px; border: 1px solid var(--line); text-align: left; vertical-align: top; } .specs th { width: 30%; font-size: 0.8rem; letter-spacing: 0.5px; color: var(--muted); } .list { table-layout: fixed; } -.list td:first-child { width: 45%; } -.list td:nth-child(2) { width: 12%; } -.list td:nth-child(4) { width: 15%; } +.list :is(th, td):first-child { width: 45%; } +.list :is(th, td):nth-child(2) { width: 12%; } +.list :is(th, td):nth-child(4) { width: 15%; } .list { font-size: 0.95rem; } -@media (max-width: 600px) { .list td:nth-child(4) { display: none; } } +@media (max-width: 600px) { .list :is(th, td):nth-child(4) { display: none; } } +.list thead th { font-size: 0.8rem; letter-spacing: 0.5px; color: var(--muted); } +.content th { padding: 6px 10px; text-align: left; } +.sortable th { cursor: pointer; user-select: none; white-space: nowrap; } +.sortable th:hover, .sortable th:focus-visible { color: var(--text, inherit); text-decoration: underline; } +.sortable th::after { content: " ↕"; opacity: 0.4; } +.sortable th[aria-sort="ascending"]::after { content: " ↑"; opacity: 1; } +.sortable th[aria-sort="descending"]::after { content: " ↓"; opacity: 1; } .gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; } .gallery figure, .thumbs a, .card, .comments li { diff --git a/assets/js/sortTables.js b/assets/js/sortTables.js new file mode 100644 index 00000000..ab894e52 --- /dev/null +++ b/assets/js/sortTables.js @@ -0,0 +1,53 @@ +// Tables with a header row can be re-ordered by clicking a column header. +(function () { + function headerRow(table) { + if (table.tHead && table.tHead.rows.length) return table.tHead.rows[table.tHead.rows.length - 1]; + var first = table.rows[0]; + if (!first || first.cells.length < 2) return null; + for (var i = 0; i < first.cells.length; i++) if (first.cells[i].tagName !== "TH") return null; + return first; + } + + function value(row, col) { + var cell = row.cells[col]; + return cell ? cell.textContent.trim() : ""; + } + + function number(text) { + return /^[-+]?[\d.,\s]*\d[\d.,\s]*\S{0,3}$/.test(text) ? parseFloat(text.replace(/[\s,]/g, "")) : NaN; + } + + function sort(table, head, th) { + var col = th.cellIndex, down = th.getAttribute("aria-sort") === "ascending"; + var rows = []; + for (var b = 0; b < table.tBodies.length; b++) + for (var r = 0; r < table.tBodies[b].rows.length; r++) + if (table.tBodies[b].rows[r] !== head) rows.push(table.tBodies[b].rows[r]); + if (!rows.length) return; + var numeric = rows.every(function (row) { var v = value(row, col); return !v || !isNaN(number(v)); }); + var compare = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare; + rows.map(function (row, i) { return { row: row, text: value(row, col), i: i }; }) + .sort(function (a, b) { + if (!a.text || !b.text) return a.text || b.text ? (a.text ? -1 : 1) : a.i - b.i; // empty cells last + var d = numeric ? number(a.text) - number(b.text) : compare(a.text, b.text); + return (down ? -d : d) || a.i - b.i; + }) + .forEach(function (item) { rows[0].parentNode.appendChild(item.row); }); + for (var i = 0; i < head.cells.length; i++) head.cells[i].removeAttribute("aria-sort"); + th.setAttribute("aria-sort", down ? "descending" : "ascending"); + } + + document.querySelectorAll("table").forEach(function (table) { + var head = headerRow(table); + if (!head || table.querySelector("[rowspan], [colspan]")) return; + table.classList.add("sortable"); + Array.prototype.forEach.call(head.cells, function (th) { + th.tabIndex = 0; + th.title = "Sort by this column"; + th.addEventListener("click", function () { sort(table, head, th); }); + th.addEventListener("keydown", function (e) { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); sort(table, head, th); } + }); + }); + }); +})(); diff --git a/layouts/_partials/page-list.html b/layouts/_partials/page-list.html index 37f7cf1c..4a9fbf85 100644 --- a/layouts/_partials/page-list.html +++ b/layouts/_partials/page-list.html @@ -1,14 +1,22 @@ -{{/* table of pages: machines get company/year/type columns; companies are a plain list */}} -{{ if eq (index . 0).Section "companies" }} +{{/* table of pages: machines get company/year/type columns; companies are a plain list. + Takes the pages, or (dict "pages" … "head" true) to add the column headers (sortable). */}} +{{ $pages := . }}{{ $head := false }} +{{ if reflect.IsMap . }}{{ $pages = .pages }}{{ $head = .head }}{{ end }} +{{ if eq (index $pages 0).Section "companies" }}
| Name | Year | {{ if eq (index $pages 0).Section "software" }}Tags{{ else }}Type{{ end }} | Origin |
|---|---|---|---|
| {{ .Title }} | {{ partial "terms.html" (dict "page" . "taxonomy" "years") }} | diff --git a/layouts/baseof.html b/layouts/baseof.html index e145d74c..2dfee94c 100644 --- a/layouts/baseof.html +++ b/layouts/baseof.html @@ -8,6 +8,9 @@ {{ with resources.Get "css/main.css" | minify | fingerprint }} {{ end }} + {{ with resources.Get "js/sortTables.js" | minify | fingerprint }} + + {{ end }}