Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 22 additions & 20 deletions site.generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -408,29 +408,31 @@ private String index(final Parser parser, final HtmlRenderer renderer,
height: 400px;
margin: 1rem 0 1rem 0;
}
th.sortable {
cursor: pointer;
user-select: none;
}
th.sortable:hover {
background-color: #f0f0f0;
}
.sort-indicator {
color: #999;
font-size: 0.85em;
margin-left: 4px;
}
th.sort-active .sort-indicator {
color: inherit;
}
</style>
</head>
<body>""" +
patchedBody +
" <script>\n" +
" window.addEventListener('DOMContentLoaded', function () {\n" +
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function moved into site.js.

Having the javascript in its own file make it easier to read it and evolve it. And then we can just inject the whole file when generating.

" var conferenceFilter = document.querySelector('input[data-filter=\"conferences\"]');\n" +
" var conferenceTable = document.getElementById('conferences');\n" +
" var conferenceTableTrs = Array.from(conferenceTable.querySelectorAll('tbody > tr'))\n" +
" .map(function (e) {\n" +
" return {text: (e.innerText || e.textContent).toLowerCase(), element: e, display: e.style.display};\n" +
" });\n" +
" conferenceFilter.addEventListener('keyup', function (e) {\n" + // todo: debounce? not critical yet
" var filter = (conferenceFilter.value || '').toLowerCase().split(' ');\n" + // todo: support AND/OR keywords?
" conferenceTableTrs.forEach(function (data) {\n" +
" data.element.style.display = filter.some(function (it) { return data.text.indexOf(it) >= 0; }) ?\n" +
" data.display : 'none';\n" +
" });\n" +
" });\n" +
" });\n" +
" </script>\n" +
"</body>\n" +
"</html>\n";
patchedBody + """
<script>
""" + Files.readString(Path.of("site.js")) + """
</script>
</body>
</html>
""";
}

private String injectTableFilter(final String html) {
Expand Down
110 changes: 110 additions & 0 deletions site.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
window.addEventListener('DOMContentLoaded', function () {
// --- Filter ---
var conferenceFilter = document.querySelector('input[data-filter="conferences"]');
var conferenceTable = document.getElementById('conferences');
var conferenceTableTrs = Array.from(conferenceTable.querySelectorAll('tbody > tr'))
.map(function (e) {
return { text: (e.innerText || e.textContent).toLowerCase(), element: e, display: e.style.display };
});
conferenceFilter.addEventListener('keyup', function () {
var filter = (conferenceFilter.value || '').toLowerCase().split(' ');
conferenceTableTrs.forEach(function (data) {
data.element.style.display = filter.some(function (it) { return data.text.indexOf(it) >= 0; })
? data.display
: 'none';
});
});

// --- Sortable headers ---
var MONTHS = {
january: 1, february: 2, march: 3, april: 4, may: 5, june: 6,
july: 7, august: 8, september: 9, october: 10, november: 11, december: 12,
jan: 1, feb: 2, mar: 3, apr: 4, jun: 6, jul: 7, aug: 8, sep: 9, sept: 9, oct: 10, nov: 11, dec: 12
};

function parseDate(text) {
// matches "22-27 January 2026", "17–19 March 2026", "26 January 2025"
var m = text.match(/(\d+)\s*[–\-]?\s*\d*\s*([A-Za-z]+)\s+(\d{4})/);
if (!m) return Infinity;
var mo = MONTHS[m[2].toLowerCase()];
if (!mo) return Infinity;
return parseInt(m[3], 10) * 10000 + mo * 100 + parseInt(m[1], 10);
}

function parseCfpDate(text) {
// matches "Closes 31 May 2026", "Closed 30 September 2025", "closed November 2025" (no day)
var m = text.match(/Clos(?:es|ed)\s+(?:(\d+)\s+)?([A-Za-z]+)\s+(\d{4})/i);
if (!m) return Infinity;
var mo = MONTHS[m[2].toLowerCase()];
if (!mo) return Infinity;
var day = m[1] ? parseInt(m[1], 10) : 1;
return parseInt(m[3], 10) * 10000 + mo * 100 + day;
}

function detectType(headerText) {
var t = headerText.toLowerCase();
if (t.indexOf('date') >= 0) return 'date';
if (t.indexOf('cfp') >= 0) return 'cfp';
return 'text';
}

function getKey(td, type) {
var text = (td.innerText || td.textContent || '').trim();
if (type === 'date') return parseDate(text);
if (type === 'cfp') return parseCfpDate(text);
return text.toLowerCase();
}

function cmpKey(a, b) {
if (a === b) return 0;
if (a === Infinity) return 1;
if (b === Infinity) return -1;
return a < b ? -1 : 1;
}

Array.from(document.querySelectorAll('table')).forEach(function (table) {
var tbody = table.querySelector('tbody');
if (!tbody) return;
var ths = Array.from(table.querySelectorAll('thead th'));
if (ths.length === 0) return;

var state = { col: -1, dir: 1 };

ths.forEach(function (th, idx) {
th.classList.add('sortable');
var indicator = document.createElement('span');
indicator.className = 'sort-indicator';
indicator.textContent = '⇅';
th.appendChild(indicator);
var type = detectType(th.textContent);

th.addEventListener('click', function () {
if (state.col === idx) {
state.dir = -state.dir;
} else {
state.col = idx;
state.dir = 1;
}

var rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort(function (r1, r2) {
var c1 = r1.cells[idx], c2 = r2.cells[idx];
if (!c1 || !c2) return 0;
return cmpKey(getKey(c1, type), getKey(c2, type)) * state.dir;
});
rows.forEach(function (r) { tbody.appendChild(r); });

ths.forEach(function (h, i) {
var ind = h.querySelector('.sort-indicator');
if (i === idx) {
ind.textContent = state.dir > 0 ? '▲' : '▼';
h.classList.add('sort-active');
} else {
ind.textContent = '⇅';
h.classList.remove('sort-active');
}
});
});
});
});
});