www-from-model/static/publications.js
Stephan Heunis 066c879dfe
Some checks failed
Deploy on webserver / Build site and deploy on success (push) Failing after 1s
New approach to extracting publication data and filtering/searching it
This commit introduces the process for individual publication term page
generation, which includes a new jinja template and workflow. Since
'publications' is a taxonomy, the workflow renders the term page at
'content/publications/pub-pid/_index.md'. For the same reason, the
(more) correct html template to use would be 'publications/taxonomy.html'
instead of 'publications/list.html' (which is actually the fallback),
hence the replacement.

Since, individual publication metadata are included in each individual
term page front matter, the taxonomy page has been updated to grab that
metadata instead of reading the json object from 'data/publications.json'.
This also allows the associated workflow to be dropped.

A new 'publication-item' partial is introduced to allow better control
over the rendering of individual items in the publication list on the
taxonomy page. This intentionally shifts the rendering that was previously
done in JS code to Hugo templating. Because hugo only runs the rendering
on app build, the searching/filtering approach had to be changed so that
all publications are rendered by default and required items are hidden
depending on the filtering options selected or search terms entered.
This is done in updated JS code, by assigning 'display: none' when
applicable. Other JS additions include:
- adding a count of filtered items
- a new 'Clear all' button for clearing filters
- allowing Topic pills to be clicked in order to add filter options

TODO: customize the publications term page template for improved individual
publication display.
2026-04-13 10:53:44 +02:00

175 lines
No EOL
4.6 KiB
JavaScript

// Load data from all pre-rendered publication item divs
const publications = Array.from(document.querySelectorAll(".pub"));
// State
let state = {
search: "",
kind: new Set(),
topic: new Set(),
year: new Set()
};
// Build filter options dynamically
function getUniqueValues(field) {
const values = new Set();
publications.forEach(el => {
if (field === "topic") {
getTopics(el).forEach(v => values.add(v));
} else {
values.add(el.dataset[field]);
}
});
return Array.from(values).sort();
}
// Format 'kind' display values
function formatValue(field, value) {
if (value) {
if (field == "kind") {
return value.split(":").at(-1)
}
}
return value
}
function renderFilter(containerId, field, values) {
const container = document.getElementById(containerId);
values.forEach(value => {
const id = `${field}-${value}`;
const label = document.createElement("label");
label.style.display = "block";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.value = value;
checkbox.dataset.field = field;
checkbox.id = id;
checkbox.addEventListener("change", () => {
if (checkbox.checked) {
state[field].add(value);
} else {
state[field].delete(value);
}
render();
});
label.appendChild(checkbox);
label.appendChild(document.createTextNode(" " + formatValue(field, value)));
container.appendChild(label);
});
}
// Filtering logic
function matchesFilters(el) {
const kind = el.dataset.kind;
const year = el.dataset.year;
const topics = getTopics(el);
// kind filter
if (state.kind.size && !state.kind.has(kind)) {
return false;
}
// year filter
if (state.year.size && !state.year.has(year)) {
return false;
}
// topic filter (multi-match)
if (state.topic.size) {
const match = topics.some(t => state.topic.has(t));
if (!match) return false;
}
return true;
}
// Helper to get topics from an element
function getTopics(el) {
try {
return JSON.parse(el.dataset.topics || "[]").map(t => t.display_label);
} catch {
return [];
}
}
// Search logic
function matchesSearch(el) {
if (!state.search) return true;
const text = (
el.dataset.title + " " +
el.dataset.kind + " " +
el.dataset.year + " " +
el.dataset.topics + " " +
el.dataset.authors
).toLowerCase();
return text.includes(state.search);
}
// Change display of divs based on filtering/searching
function render() {
let count = 0;
publications.forEach(el => {
const visible =
matchesFilters(el) &&
matchesSearch(el);
if (visible) count+=1;
renderCount(count)
el.style.display = visible ? "" : "none";
});
}
// Count of searched+filtered publications
function renderCount(count) {
const countEl = document.getElementById('pub-count');
countEl.innerHTML = `${count}` ;
}
// Set checkbox if user clicks on topic pill
function selectFilter(field, value) {
const checkbox = document.querySelector(
`input[type="checkbox"][data-field="${field}"][value="${CSS.escape(value)}"]`
);
if (!checkbox) return;
if (!checkbox.checked) {
checkbox.checked = true;
state[field].add(value);
render();
}
}
// Clear all
function clearAllFilters() {
// Reset state
state.kind.clear();
state.topic.clear();
state.year.clear();
state.search = "";
// Uncheck all checkboxes
document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = false;
});
// Clear search input
const searchInput = document.getElementById("search");
if (searchInput) {
searchInput.value = "";
}
// Re-render results
render();
}
// On startup:
// 1) Build filters
renderFilter("filter-kind", "kind", getUniqueValues("kind"));
renderFilter("filter-topic", "topic", getUniqueValues("topic"));
renderFilter("filter-year", "year", getUniqueValues("year").map(String));
// 2) Register search input
document.getElementById("search").addEventListener("input", e => {
state.search = e.target.value.toLowerCase();
render();
});
// 3) Add topic click handlers
document.querySelectorAll(".topic-chip").forEach(el => {
el.addEventListener("click", () => {
const topic = el.dataset.topic;
selectFilter("topic", topic);
});
});
// 4) Initial render
render();