(function () { // === 1) Date / day-of-year (for deterministic highlight pick) === const today = new Date(); const formattedDate = today.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); function getDayOfYear(date) { const start = new Date(date.getFullYear(), 0, 0); return Math.floor((date - start) / 86400000); } const dayOfYear = getDayOfYear(today); // === 2) DOM refs for the highlight block (hero) === const elDate = document.getElementById("event-date"); const elTitle = document.getElementById("event-title"); const elBody = document.getElementById("event-body"); const elImg = document.getElementById("event-image"); const elImgWrap = document.getElementById("event-image-wrap"); const elSpeaker = document.getElementById("event-speaker"); const elLanguage = document.getElementById("event-language"); const elInstitute = document.getElementById("event-institute"); if (elDate) elDate.textContent = formattedDate; // === 3) DOM refs for halls and talk lists === const hallHeadingEls = { "1": document.getElementById("hall-1-heading"), "2": document.getElementById("hall-2-heading"), "3": document.getElementById("hall-3-heading"), }; const hallTalksEls = { "1": document.getElementById("hall-1-talks"), "2": document.getElementById("hall-2-talks"), "3": document.getElementById("hall-3-talks"), }; // === 4) Load schedule JSON === fetch("text/schedule.json", { cache: "no-cache" }) .then((resp) => { if (!resp.ok) throw new Error("could not load schedule.json"); return resp.json(); }) .then((data) => { const halls = data.halls || []; const talks = data.talks || []; // 4a) Set hall headings halls.forEach((hall) => { if (hallHeadingEls[hall.id]) { hallHeadingEls[hall.id].textContent = decodeUnicode(hall.label || ("Hörsaal " + hall.id)); } }); // 4b) Group talks by hall id const groupedByHall = {}; talks.forEach((talk) => { if (!groupedByHall[talk.hall]) groupedByHall[talk.hall] = []; groupedByHall[talk.hall].push(talk); }); // 4c) Desktop: gemeinsame Zeitliste + Platzhalter, Mobile: wie bisher const timesOrder = uniqueTimesInOrder(talks); // in Reihenfolge aus JSON Object.keys(hallTalksEls).forEach((hallId) => { const container = hallTalksEls[hallId]; if (!container) return; const hallTalks = groupedByHall[hallId] || []; // Index nach Zeit für schnellen Zugriff const byTime = new Map(hallTalks.map(t => [String(t.time || "").trim(), t])); // Desktop nebeneinander -> gleiche Zeilen je Zeit; sonst originale Liste const desktop = window.matchMedia("(min-width: 992px)").matches; let rows = []; if (desktop) { rows = timesOrder.map((time, i) => { const talk = byTime.get(time) || null; return renderTalkRow(talk, hallId, i, time); }); } else { rows = hallTalks.map((t, i) => renderTalkRow(t, hallId, i, t.time || "")); } container.innerHTML = rows.join(""); wireImageLoadReflow(container); }); // 4d) Enable toggle animation wireTalkToggles(); // 4e) Zeilenhöhen anpassen (nur Desktop) equalizeRowHeightsOnce(); // 4e) Fill the highlight block deterministically // Only talks that have BOTH a non-empty description and a real image const highlightCandidates = (talks || []).filter((t) => { const hasDesc = (t.desc || "").trim().length > 0; const hasImg = isImageUrl((t.image || "").trim()); return hasDesc && hasImg; }); if (highlightCandidates.length > 0) { const idx = dayOfYear % highlightCandidates.length; const chosen = highlightCandidates[idx]; const highlightTitle = decodeUnicode(chosen.title || "Untitled"); const highlightBody = chosen.desc || highlightTitle; const highlightImage = chosen.image || ""; const highlightAlt = decodeUnicode(chosen.imageAlt || highlightTitle || "Image"); if (elTitle) elTitle.textContent = highlightTitle; if (elBody) elBody.innerHTML = formatHtmlWithBreaks(highlightBody); if (elImg) { elImg.alt = highlightAlt; if (isImageUrl(highlightImage)) { elImg.src = highlightImage; elImg.style.display = ""; } else { elImg.removeAttribute("src"); elImg.style.display = "none"; } } if (elSpeaker) { const profTitle = chosen.profTitle || ""; const lecturer = chosen.lecturer || ""; const instLabel = chosen.instituteLabel || ""; const instUrl = chosen.instituteUrl || ""; const name = (profTitle ? profTitle.trim() + " " : "") + (lecturer || ""); const inst = instUrl ? `${escapeText( instLabel || instUrl )}` : instLabel ? `${escapeText(instLabel)}` : ""; const sep = name && inst ? ", " : ""; elSpeaker.innerHTML = `Referent: ${escapeText( name.trim() )}${sep}${inst}`; } if (elLanguage && (chosen.language || "")) { elLanguage.innerHTML = `Sprache: ${escapeText( chosen.language )}`; } if (elInstitute) { const instLabel = chosen.instituteLabel || ""; const instUrl = chosen.instituteUrl || ""; if (elInstitute.tagName === "A" && instUrl) { elInstitute.textContent = decodeUnicode(instLabel || instUrl); elInstitute.setAttribute("href", instUrl); } else { elInstitute.textContent = decodeUnicode(instLabel || ""); } } } else { // No suitable highlight -> clear/hide gracefully if (elTitle) elTitle.textContent = " "; if (elBody) elBody.textContent = " "; if (elImg) { elImg.removeAttribute("src"); elImg.alt = ""; elImg.style.display = "none"; } if (elSpeaker) elSpeaker.textContent = ""; if (elLanguage) elLanguage.textContent = ""; if (elInstitute) elInstitute.textContent = ""; } }) .catch((err) => { console.error("schedule load error:", err); if (elTitle) elTitle.textContent = "Highlight could not be loaded"; if (elBody) elBody.textContent = "Please try again later."; if (elImg) { elImg.alt = "No image available"; elImg.style.display = "none"; } Object.values(hallTalksEls).forEach((container) => { if (container) { container.innerHTML = `

--:--

`; } }); }); // === Helpers === // Escape for text node contexts function escapeText(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } // Escape for attribute values function escapeAttr(str) { return escapeText(str).replace(/'/g, "'"); } // Decode literals like "U+00AE" or "u+1F4A9" to real Unicode function decodeUnicode(str) { if (!str) return ""; return String(str).replace(/U\+([0-9A-F]{4,6})/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)) ); } // Produce HTML: decode "U+XXXX", escape HTML, then turn newlines into
function formatHtmlWithBreaks(str) { const decoded = decodeUnicode(str).replace(/\r\n?/g, "\n"); const escaped = escapeText(decoded); return escaped.replace(/\n/g, "
"); } // Accept only real image files; PDFs etc. are rejected for highlight/images function isImageUrl(url) { if (!url) return false; return /\.(png|jpe?g|webp|gif|avif|svg)$/i.test(url); } // Build an i18n-ready "Speaker" line. function buildLecturerLine({ profTitle = "", lecturer = "", instLabel = "", instUrl = "", }) { if (!profTitle && !lecturer && !instLabel) return ""; const name = (profTitle ? escapeText(decodeUnicode(profTitle.trim())) + " " : "") + escapeText(decodeUnicode(lecturer || "").trim()); const inst = instUrl ? `${escapeText( decodeUnicode(instLabel || instUrl) )}` : instLabel ? `${escapeText(decodeUnicode(instLabel))}` : ""; const sep = name && inst ? ", " : ""; return `

Referent: ${name}${sep}${inst}

`; } // Build an i18n-ready "Language" line. function buildLanguageLine(langValue = "") { if (!langValue) return ""; return `

Sprache: ${escapeText( decodeUnicode(langValue) )}

`; } // Toggle animation and a11y attributes for each talk block function wireTalkToggles() { const toggles = document.querySelectorAll(".talk-toggle"); toggles.forEach((btn) => { btn.addEventListener("click", () => { const panelId = btn.getAttribute("aria-controls"); const panel = document.getElementById(panelId); if (!panel) return; const expanded = btn.getAttribute("aria-expanded") === "true"; const nowExpanded = !expanded; // a11y attributes btn.setAttribute("aria-expanded", String(nowExpanded)); panel.setAttribute("aria-hidden", String(!nowExpanded)); // animate max-height + opacity if (nowExpanded) { // open just this panel panel.classList.add("is-open"); panel.style.maxHeight = panel.scrollHeight + "px"; panel.style.opacity = "1"; } else { // close just this panel panel.style.maxHeight = panel.scrollHeight + "px"; // start from current height panel.getBoundingClientRect(); // force reflow panel.classList.remove("is-open"); panel.style.maxHeight = "0"; panel.style.opacity = "0"; } // IMPORTANT: // Do NOT re-equalize heights here. We want other columns to stay unchanged. }); }); } // Liefert die Zeitpunkte in der Reihenfolge ihres ersten Auftretens im JSON function uniqueTimesInOrder(talks) { const seen = new Set(); const out = []; talks.forEach(t => { const tt = String(t.time || "").trim(); if (tt && !seen.has(tt)) { seen.add(tt); out.push(tt); } }); return out; } // Eine Zeile (Talk oder leerer Platzhalter für eine Zeit) function renderTalkRow(talk, hallId, i, timeLabel) { const safeTime = timeLabel || (talk ? talk.time || "" : ""); if (!talk) { // leerer Platzhalter – sorgt für fluchtende Zeiten und gleiche Spaltenhöhe return `

${escapeText(safeTime)}

`; } const safeTitle = decodeUnicode(talk.title || ""); const rawDesc = talk.desc || ""; const safeDesc = rawDesc.trim() !== "" ? rawDesc : ""; const imgUrl = (talk.image || "").trim(); const safeImg = isImageUrl(imgUrl) ? imgUrl : ""; const safeAlt = talk.imageAlt || safeTitle || "Talk image"; const profTitle = talk.profTitle || ""; const lecturer = talk.lecturer || ""; const instLabel = talk.instituteLabel || ""; const instUrl = talk.instituteUrl || ""; const language = talk.language || ""; const panelId = `talk-details-${hallId}-${i}`; const lecturerLine = buildLecturerLine({ profTitle, lecturer, instLabel, instUrl }); const languageLine = buildLanguageLine(language); const detailsInner = `
${safeDesc ? `

${formatHtmlWithBreaks(safeDesc)}

` : ""} ${lecturerLine} ${languageLine} ${safeImg ? `${escapeAttr(decodeUnicode(safeAlt))}` : ""}
`; const hasDetails = safeDesc || lecturerLine || languageLine || safeImg; const toggleButton = hasDetails ? `` : `
${escapeText(safeTitle)}
`; return `

${escapeText(safeTime)}

${toggleButton} ${hasDetails ? ` ` : ""}
`; } /** * Equalize row heights ONCE, right after initial render while all panels are collapsed. * We set min-height so that opening one panel later will not change the other columns. */ function equalizeRowHeightsOnce() { const desktop = window.matchMedia("(min-width: 992px)").matches; const columns = ["1", "2", "3"] .map((id) => document.getElementById(`hall-${id}-talks`)) .filter(Boolean); // Reset any previous min-heights (needed if you re-render programmatically) columns.forEach((col) => { col.querySelectorAll(".talk-block").forEach((b) => (b.style.minHeight = "")); }); if (!desktop || columns.length === 0) return; const colRows = columns.map((col) => Array.from(col.querySelectorAll(".talk-block")) ); const maxRows = Math.max(...colRows.map((r) => r.length)); // For each logical time-row, measure the tallest collapsed block and apply as min-height for (let i = 0; i < maxRows; i++) { const rowElems = colRows.map((r) => r[i]).filter(Boolean); const maxH = Math.max(...rowElems.map((el) => el.offsetHeight)); rowElems.forEach((el) => (el.style.minHeight = maxH + "px")); } } // kleines Debounce function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), ms); }; } // Nach dem Auf-/Zuklappen neu ausrichten const _origWire = wireTalkToggles; wireTalkToggles = function () { const toggles = document.querySelectorAll(".talk-toggle"); toggles.forEach((btn) => { btn.addEventListener("click", () => { const panelId = btn.getAttribute("aria-controls"); const panel = document.getElementById(panelId); if (!panel) return; const expanded = btn.getAttribute("aria-expanded") === "true"; const nowExpanded = !expanded; btn.setAttribute("aria-expanded", String(nowExpanded)); panel.setAttribute("aria-hidden", String(!nowExpanded)); if (nowExpanded) { panel.classList.add("is-open"); panel.style.maxHeight = panel.scrollHeight + "px"; panel.style.opacity = "1"; } else { panel.style.maxHeight = panel.scrollHeight + "px"; panel.getBoundingClientRect(); panel.classList.remove("is-open"); panel.style.maxHeight = "0"; panel.style.opacity = "0"; } // nach Ende der Transition neu messen setTimeout(equalizeRowHeights, 250); }); }); }; // Run once after you injected the HTML function wireImageLoadReflow(root) { const scope = root || document; scope.querySelectorAll(".talk-image").forEach((img) => { if (img.dataset._reflowBound) return; // avoid double-binding img.dataset._reflowBound = "1"; img.addEventListener("load", () => { const panel = img.closest(".talk-details"); if (panel && panel.classList.contains("is-open")) { // Recompute max-height to fit the now-loaded image panel.style.maxHeight = panel.scrollHeight + "px"; } }, { passive: true }); }); } })();