Skip to content
Open
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Defined in `_config.yml` with default layouts. Front-matter shapes for each are

- **Projects** (`_projects/{de,en}/slug.md`, layout `_layouts/project.html`) can describe *multiple project phases* in one file: the Markdown body is split on `---` (rendered as `<hr />`) and zipped with the `projects:` front-matter list, so section *N* of the body pairs with `projects[N]`'s metadata (dates, partner, funder). `category` (`ehealth` | `ml` | `cv`) drives the home-page carousels and the projects-listing filter; active vs. completed on the listing page is decided by whether `laufzeitEnd` is in the past.
- **Publications/Theses** use `output: false` (no standalone rendered page per entry) — they're listed/grouped by year on `publications.html` / `theses.html`, with an anchor `permalink` used for search/deep-linking.
- **Publication categories** are a *list* (`categories: [ml, geo]`), not the single `category` that projects use — most papers span several research areas. The taxonomy (ids plus DE/EN labels) lives only in `_data/publication_categories.yml`; both language pages are thin wrappers around `_includes/listing-filter.html` (category + year listboxes; the theses pages reuse it with `kind="theses"` for category + year + thesis-type filters; theses carry the same `categories:` list, and `thesis_type` is bachelor / master / dissertation) and `_includes/publication-list.html`, so a new category means editing the data file and the affected publications, nothing else. Both listings group entries into `.year-section`s; `initYearSections()` in `main.js` shows only two entries of every year, with a right-aligned "show N more" button (`_includes/year-more.html`) under them; the filter re-applies that rule.
- **Publication categories** are a *list* (`categories: [ml, geo]`), not the single `category` that projects use — most papers span several research areas. The taxonomy (ids plus DE/EN labels) lives only in `_data/publication_categories.yml`; both language pages are thin wrappers around `_includes/listing-filter.html` (category + year listboxes plus, for the publications only, a search field over title, reference and DOI that unfolds every year while it is in use and is kept in the hash as `q=`; the theses pages reuse it with `kind="theses"` for category + year + thesis-type filters; theses carry the same `categories:` list, and `thesis_type` is bachelor / master / dissertation) and `_includes/publication-list.html`, so a new category means editing the data file and the affected publications, nothing else. Both listings group entries into `.year-section`s; `initYearSections()` in `main.js` shows only two entries of every year, with a right-aligned "show N more" button (`_includes/year-more.html`) under them; the filter re-applies that rule.
- **Research areas** have one page each (`forschung-{ehealth,ml,cv}.md` + `-en`, `page_id: area:<id>`, `layout: area`). `_layouts/area.html` collects projects (`category contains area`, so list-valued categories count), publications and theses (`categories contains area`) itself; the pages only hold the text, optional `focus` topics (none set yet; for a list curated by the area lead) and an optional `contact` e-mail looked up in `_data/team.yml` (the sidebar shows a contact box only when it is set). They are linked from the *Projects* nav dropdown and from the area headings/links on the home page.
- **Open positions / thesis topics** (`mitarbeit.html` / `mitarbeit-en.html`, `page_id: positions`) are data-driven from `_data/positions.yml` and `_data/thesis_topics.yml`, rendered by `_includes/open-positions.html` and `_includes/thesis-topics.html`; both show an invitation instead of an empty list. Topics reuse `_includes/listing-filter.html` with `kind="topics"` (category + type, no year); `initListingFilter()` picks up every `.listing-item[data-categories]` and treats `data-type` as a space-separated list, since a topic can suit both bachelor and master students.
- **Software & Data** (`software.html` / `software-en.html`, `page_id: software`) is generated from `_data/software.yml` by `_includes/software-list.html`; entries reference their project and the publication to cite by `page_id`. The cards fold like the publication entries (`.listing-item.collapsible`, `initCollapsibles()` in `main.js`): folded they show type, name, area badges and the repository and documentation buttons. The page sits under *Projects* in `_data/nav.yml`, next to the GitHub shortcut.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Area description (Markdown)...

## Publications

Research papers, shown grouped by year with a category filter.
Research papers, shown grouped by year with a category and year filter and a search field that finds entries by author, title or DOI (every word has to match; case and accents are ignored; the search is kept in the URL as `#q=...`, so it can be linked).

- **Location:** `_publications/{de,en}/YYYY-MM-DD-title.md` (dates typically use `12-31`)

Expand Down
15 changes: 15 additions & 0 deletions _includes/listing-filter.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
`.listing-item[data-categories]`, optionally with `data-year` / `data-type` (a
space-separated list, like the categories), and the chosen
values are mirrored into the hash as `#cat=...&year=...`.

The publications also get a search field (`data-filter-search`): it matches
title, reference (authors, venue) and DOI of an entry, every word on its own,
ignoring case and accents, and is kept in the hash as `q=...`.
{% endcomment %}

{% if include.kind == "topics" %}
Expand All @@ -32,6 +36,8 @@
{% assign t_all_years = "Alle Jahre" %}
{% assign t_type = "Art" %}
{% assign t_all_types = "Alle Arten" %}
{% assign t_search = "Publikationen durchsuchen" %}
{% assign t_search_hint = "Autor*in, Titel oder DOI" %}
{% if include.kind == "topics" %}
{% assign t_results = "{n} von {total} Themen" %}
{% elsif include.kind == "theses" %}
Expand All @@ -46,6 +52,8 @@
{% assign t_all_years = "All years" %}
{% assign t_type = "Type" %}
{% assign t_all_types = "All types" %}
{% assign t_search = "Search publications" %}
{% assign t_search_hint = "Author, title or DOI" %}
{% if include.kind == "topics" %}
{% assign t_results = "{n} of {total} topics" %}
{% elsif include.kind == "theses" %}
Expand All @@ -56,6 +64,13 @@
{% endif %}

<div class="cat-filter" data-listing-filter data-results-template="{{ t_results }}">
{% if include.kind == "publications" %}
<label class="cat-filter-search">
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
<span class="visually-hidden">{{ t_search }}</span>
<input type="search" data-filter-search placeholder="{{ t_search_hint }}" autocomplete="off" spellcheck="false">
</label>
{% endif %}
<span class="cat-filter-label" id="filterLabel-cat">{{ t_cat }}</span>
<div class="cat-filter-control" data-filter-key="cat">
<button
Expand Down
57 changes: 56 additions & 1 deletion assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,54 @@ html:not(.js) .listing-toggle-icon {
box-shadow: var(--shadow-sm);
}

/* search field of the publications (title, authors, DOI) */
.cat-filter-search {
position: relative;
flex: 1 1 16rem;
max-width: 24rem;
margin: 0;
}

.cat-filter-search i {
position: absolute;
top: 50%;
left: 0.85rem;
transform: translateY(-50%);
font-size: 0.85rem;
color: var(--color-text-light);
pointer-events: none;
}

.cat-filter-search input {
width: 100%;
padding: 0.5rem 0.9rem 0.5rem 2.3rem;
background: var(--color-white);
color: var(--color-heading);
border: 1px solid var(--color-line-strong);
border-radius: var(--radius-sm);
font-family: var(--font-body);
font-size: 0.9rem;
line-height: 1.3;
}

.cat-filter-search input::placeholder {
color: var(--color-text-light);
}

.cat-filter-search input:hover {
border-color: var(--color-primary-light);
}

.cat-filter-search input:focus-visible {
outline: none;
border-color: var(--color-primary);
box-shadow: var(--ring);
}

.cat-filter-search + .cat-filter-label {
margin-left: 0.75rem;
}

.cat-filter-label {
font-size: 0.85rem;
font-weight: var(--font-weight-bold);
Expand Down Expand Up @@ -2288,10 +2336,17 @@ html:not(.js) .listing-toggle-icon {
width: 100%;
}

.cat-filter-label + .cat-filter-control + .cat-filter-label {
.cat-filter-label + .cat-filter-control + .cat-filter-label,
.cat-filter-search + .cat-filter-label {
margin: 0.5rem 0 0;
}

/* in the column the flex basis would be its height */
.cat-filter-search {
flex: none;
max-width: none;
}

.cat-filter-results {
margin-left: 0;
}
Expand Down
59 changes: 52 additions & 7 deletions assets/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@
// that unfolds the rest (and folds it again). The filter re-applies that
// rule to the entries it leaves visible (initListingFilter); a year the
// visitor unfolded stays unfolded, and a deep link unfolds the year it
// points into.
// points into. While a search is active every year shows all its hits.
var YEAR_TEASER = 2;
var yearsUnfolded = false;

function yearVisibleItems(section) {
return Array.prototype.filter.call(section.querySelectorAll(".listing-item"), function (item) {
Expand All @@ -59,15 +60,15 @@

function renderYearSection(section) {
var items = yearVisibleItems(section);
var expanded = section.getAttribute("data-expanded") === "true";
var expanded = yearsUnfolded || section.getAttribute("data-expanded") === "true";
var extra = items.length - YEAR_TEASER;
items.forEach(function (item, i) {
item.classList.toggle("is-beyond", !expanded && i >= YEAR_TEASER);
});
var bar = section.querySelector(".year-more-bar");
var button = section.querySelector(".year-more");
if (!bar || !button) return;
bar.hidden = extra <= 0;
bar.hidden = yearsUnfolded || extra <= 0;
button.setAttribute("aria-expanded", expanded ? "true" : "false");
var text = button.querySelector(".year-more-text");
var label = button.getAttribute(expanded ? "data-label-less" : "data-label-more") || "";
Expand Down Expand Up @@ -540,8 +541,9 @@
// data-categories list, every other facet against data-<key>, which may be
// a space-separated list as well (a topic for bachelor and master
// students). An entry is shown when it matches
// every facet; year headings without visible entries are hidden. State is
// mirrored into the hash (#cat=ml&year=2024).
// every facet; year headings without visible entries are hidden. The
// publications add a search field: every word has to appear in the title,
// reference or DOI. State is mirrored into the hash (#cat=ml&year=2024&q=...).
function initListingFilter() {
var root = document.querySelector("[data-listing-filter]");
if (!root) return;
Expand All @@ -550,12 +552,33 @@
var items = Array.prototype.slice.call(document.querySelectorAll(".listing-item[data-categories]"));
var sections = Array.prototype.slice.call(document.querySelectorAll(".year-section"));
var boxes = {};
var search = root.querySelector("[data-filter-search]");

// lower case without accents, so "muller" also finds "Müller"
function fold(text) {
return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
}

function textOf(item, selector) {
var el = item.querySelector(selector);
return el ? el.textContent : "";
}

items.forEach(function (item) {
var raw = item.getAttribute("data-categories") || "";
item.catList = raw.split(" ").filter(Boolean);
// title, reference (authors, venue) and DOI
if (search) {
item.searchText = fold([".listing-toggle-text", ".publication-meta", ".doi-link"].map(function (sel) {
return textOf(item, sel);
}).join(" ").replace(/\s+/g, " "));
}
});

function searchTerms() {
return search ? fold(search.value).split(/\s+/).filter(Boolean) : [];
}

function matches(item, key, value) {
if (value === "all") return true;
if (key === "cat") return item.catList.indexOf(value) !== -1;
Expand All @@ -564,20 +587,24 @@

function apply() {
var keys = Object.keys(boxes);
var terms = searchTerms();
var shown = 0;

items.forEach(function (item) {
var ok = keys.every(function (key) {
return matches(item, key, boxes[key].value());
}) && terms.every(function (term) {
return item.searchText.indexOf(term) !== -1;
});
item.style.display = ok ? "" : "none";
if (ok) shown++;
});

// a year without visible entries disappears
// a year without visible entries disappears; a search shows all hits
sections.forEach(function (section) {
section.style.display = yearVisibleItems(section).length ? "" : "none";
});
yearsUnfolded = terms.length > 0;
applyYearDefaults();

var tpl = root.getAttribute("data-results-template") || "";
Expand All @@ -592,6 +619,7 @@
var v = boxes[key].value();
if (v !== "all") parts.push(key + "=" + v);
});
if (search && search.value.trim()) parts.push("q=" + encodeURIComponent(search.value.trim()));
// replaceState so filtering does not pile up history entries
history.replaceState(null, "",
parts.length ? "#" + parts.join("&") : location.pathname + location.search);
Expand All @@ -618,11 +646,28 @@
});
}

// Restore facets from the hash, e.g. #cat=ml&year=2024.
if (search) {
search.addEventListener("input", function () {
apply();
syncHash();
});
}

// Restore facets from the hash, e.g. #cat=ml&year=2024&q=praschl.
Object.keys(boxes).forEach(function (key) {
var m = new RegExp("(?:^#|&)" + key + "=([a-z0-9]+)").exec(location.hash);
boxes[key].set(m ? m[1] : "all", false);
});
if (search) {
var q = /(?:^#|&)q=([^&]*)/.exec(location.hash);
if (q) {
try {
search.value = decodeURIComponent(q[1]);
} catch (e) {
search.value = "";
}
}
}
apply();
}

Expand Down
Loading