attribution image

This commit is contained in:
Simon Martens
2025-09-28 15:02:24 +02:00
parent 0329ba5f5c
commit 2318c0c06d
6 changed files with 2624 additions and 431 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

731
views/assets/Europe.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@@ -2,7 +2,7 @@ document.body.addEventListener("htmx:configRequest", function(a) {
let e = a.detail.elt; let e = a.detail.elt;
e.id === "search" && e.value === "" && (a.detail.parameters = {}, a.detail.path = window.location.pathname + window.location.search); e.id === "search" && e.value === "" && (a.detail.parameters = {}, a.detail.path = window.location.pathname + window.location.search);
}); });
class N extends HTMLElement { class $ extends HTMLElement {
constructor() { constructor() {
super(); super();
} }
@@ -35,8 +35,8 @@ class N extends HTMLElement {
}); });
} }
} }
customElements.define("person-jump-filter", N); customElements.define("person-jump-filter", $);
class $ extends HTMLElement { class O extends HTMLElement {
connectedCallback() { connectedCallback() {
const e = this.querySelector("#place-search"); const e = this.querySelector("#place-search");
e && e.addEventListener("input", (t) => { e && e.addEventListener("input", (t) => {
@@ -49,8 +49,8 @@ class $ extends HTMLElement {
}); });
} }
} }
customElements.define("place-jump-filter", $); customElements.define("place-jump-filter", O);
class O extends HTMLElement { class V extends HTMLElement {
connectedCallback() { connectedCallback() {
const e = this.querySelector("#category-search"); const e = this.querySelector("#category-search");
e && e.addEventListener("input", (t) => { e && e.addEventListener("input", (t) => {
@@ -63,7 +63,7 @@ class O extends HTMLElement {
}); });
} }
} }
customElements.define("category-jump-filter", O); customElements.define("category-jump-filter", V);
class R extends HTMLElement { class R extends HTMLElement {
constructor() { constructor() {
super(), this.issuesByYear = {}; super(), this.issuesByYear = {};
@@ -180,7 +180,7 @@ class R extends HTMLElement {
} }
} }
customElements.define("year-jump-filter", R); customElements.define("year-jump-filter", R);
class V extends HTMLElement { class z extends HTMLElement {
constructor() { constructor() {
super(), this.isOpen = !1; super(), this.isOpen = !1;
} }
@@ -242,8 +242,8 @@ class V extends HTMLElement {
this.isOpen && t && i && !t.contains(e.target) && !this.contains(e.target) && this.hideFilter(); this.isOpen && t && i && !t.contains(e.target) && !this.contains(e.target) && this.hideFilter();
} }
} }
customElements.define("schnellauswahl-button", V); customElements.define("schnellauswahl-button", z);
class z extends HTMLElement { class D extends HTMLElement {
constructor() { constructor() {
super(), this.isOpen = !1; super(), this.isOpen = !1;
} }
@@ -319,7 +319,7 @@ class z extends HTMLElement {
this.isOpen && !this.contains(e.target) && this.hideMenu(); this.isOpen && !this.contains(e.target) && this.hideMenu();
} }
} }
customElements.define("navigation-menu", z); customElements.define("navigation-menu", D);
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("click", function(a) { document.addEventListener("click", function(a) {
const e = a.target.closest('a[href^="/akteure/"], a[href^="/ort/"]'), t = document.getElementById("filter-container"); const e = a.target.closest('a[href^="/akteure/"], a[href^="/ort/"]'), t = document.getElementById("filter-container");
@@ -337,13 +337,13 @@ document.addEventListener("DOMContentLoaded", function() {
} }
}); });
}); });
const T = []; const k = [];
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
I(); I();
}); });
const I = function() { const I = function() {
for (; T.length > 0; ) { for (; k.length > 0; ) {
const a = T.shift(); const a = k.shift();
try { try {
a(); a();
} catch (e) { } catch (e) {
@@ -492,9 +492,9 @@ class F extends HTMLElement {
document.documentElement.offsetHeight document.documentElement.offsetHeight
), s = window.innerHeight, o = n - s, r = o > 0 ? window.scrollY / o : 0, l = t.clientHeight, d = t.scrollHeight - l; ), s = window.innerHeight, o = n - s, r = o > 0 ? window.scrollY / o : 0, l = t.clientHeight, d = t.scrollHeight - l;
if (d > 0) { if (d > 0) {
const u = r * d, h = i.getBoundingClientRect(), p = t.getBoundingClientRect(), g = h.top - p.top + t.scrollTop, m = l / 2, y = g - m, w = 0.7, v = w * u + (1 - w) * y, x = Math.max(0, Math.min(d, v)), E = t.scrollTop; const u = r * d, h = i.getBoundingClientRect(), p = t.getBoundingClientRect(), b = h.top - p.top + t.scrollTop, g = l / 2, y = b - g, w = 0.7, x = w * u + (1 - w) * y, v = Math.max(0, Math.min(d, x)), E = t.scrollTop;
Math.abs(x - E) > 10 && t.scrollTo({ Math.abs(v - E) > 10 && t.scrollTo({
top: x, top: v,
behavior: "smooth" behavior: "smooth"
}); });
} }
@@ -514,7 +514,7 @@ class F extends HTMLElement {
} }
} }
customElements.define("akteure-scrollspy", F); customElements.define("akteure-scrollspy", F);
class D extends HTMLElement { class j extends HTMLElement {
constructor() { constructor() {
super(), this.searchInput = null, this.placeCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0; super(), this.searchInput = null, this.placeCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0;
} }
@@ -566,9 +566,9 @@ class D extends HTMLElement {
this.countElement && (t === "" ? this.countElement.textContent = `Alle Orte (${this.originalCount})` : e === 0 ? this.countElement.textContent = `Keine Orte gefunden für "${t}"` : this.countElement.textContent = `${e} von ${this.originalCount} Orten`); this.countElement && (t === "" ? this.countElement.textContent = `Alle Orte (${this.originalCount})` : e === 0 ? this.countElement.textContent = `Keine Orte gefunden für "${t}"` : this.countElement.textContent = `${e} von ${this.originalCount} Orten`);
} }
} }
class j extends HTMLElement { class K extends HTMLElement {
constructor() { constructor() {
super(), this.isExpanded = !1, this.isLoading = !1, this.hasLoaded = !1; super(), this.isExpanded = !1, this.isLoading = !1, this.hasLoaded = !1, this.boundHandleClick = this.handleClick.bind(this), this.boundHandleMapClick = this.handleMapClick.bind(this), this.boundHandleHeadingHover = this.handleHeadingHover.bind(this), this.boundHandleHeadingLeave = this.handleHeadingLeave.bind(this);
} }
connectedCallback() { connectedCallback() {
this.setupAccordion(), this.setupEventListeners(), this.updateBorders(), this.setupMapEventListeners(), this.setupHoverEvents(); this.setupAccordion(), this.setupEventListeners(), this.updateBorders(), this.setupMapEventListeners(), this.setupHoverEvents();
@@ -577,7 +577,7 @@ class j extends HTMLElement {
this.cleanupEventListeners(), this.cleanupMapEventListeners(); this.cleanupEventListeners(), this.cleanupMapEventListeners();
} }
cleanupMapEventListeners() { cleanupMapEventListeners() {
document.removeEventListener("place-map-clicked", this.handleMapClick.bind(this)), this.removeEventListener("mouseenter", this.handleHeadingHover.bind(this)), this.removeEventListener("mouseleave", this.handleHeadingLeave.bind(this)); document.removeEventListener("place-map-clicked", this.boundHandleMapClick), this.removeEventListener("mouseenter", this.boundHandleHeadingHover), this.removeEventListener("mouseleave", this.boundHandleHeadingLeave);
} }
setupAccordion() { setupAccordion() {
if (!this.querySelector(".accordion-chevron")) { if (!this.querySelector(".accordion-chevron")) {
@@ -592,16 +592,16 @@ class j extends HTMLElement {
} }
} }
setupEventListeners() { setupEventListeners() {
this.addEventListener("click", this.handleClick.bind(this)); this.addEventListener("click", this.boundHandleClick);
} }
cleanupEventListeners() { cleanupEventListeners() {
this.removeEventListener("click", this.handleClick.bind(this)); this.removeEventListener("click", this.boundHandleClick);
} }
setupMapEventListeners() { setupMapEventListeners() {
document.addEventListener("place-map-clicked", this.handleMapClick.bind(this)); document.addEventListener("place-map-clicked", this.boundHandleMapClick);
} }
setupHoverEvents() { setupHoverEvents() {
this.addEventListener("mouseenter", this.handleHeadingHover.bind(this)), this.addEventListener("mouseleave", this.handleHeadingLeave.bind(this)); this.addEventListener("mouseenter", this.boundHandleHeadingHover), this.addEventListener("mouseleave", this.boundHandleHeadingLeave);
} }
handleHeadingHover() { handleHeadingHover() {
const e = this.getAttribute("data-place-id"); const e = this.getAttribute("data-place-id");
@@ -668,9 +668,9 @@ class j extends HTMLElement {
this.isExpanded ? this.classList.add("border-b", "border-slate-100") : this.classList.add("border-b", "border-slate-100"), !this.nextElementSibling && this.classList.remove("border-b"); this.isExpanded ? this.classList.add("border-b", "border-slate-100") : this.classList.add("border-b", "border-slate-100"), !this.nextElementSibling && this.classList.remove("border-b");
} }
} }
class K extends HTMLElement { class W extends HTMLElement {
constructor() { constructor() {
super(), this.places = [], this.mapElement = null, this.pointsContainer = null, this.intersectionObserver = null, this.mapPoints = /* @__PURE__ */ new Map(), this.tooltip = null, this.tooltipTimeout = null; super(), this.places = [], this.mapElement = null, this.pointsContainer = null, this.intersectionObserver = null, this.mapPoints = /* @__PURE__ */ new Map(), this.tooltip = null, this.showTimeout = null, this.hideTimeout = null, this.isTooltipVisible = !1, this.currentHoveredPlaceId = "", this.boundHandleHeadingHoverEvent = this.handleHeadingHoverEvent.bind(this);
} }
connectedCallback() { connectedCallback() {
this.parseData(), this.render(), this.initializeMap(), setTimeout(() => { this.parseData(), this.render(), this.initializeMap(), setTimeout(() => {
@@ -689,9 +689,15 @@ class K extends HTMLElement {
this.innerHTML = ` this.innerHTML = `
<div class="map-container relative w-full aspect-[5/7] overflow-hidden bg-slate-100"> <div class="map-container relative w-full aspect-[5/7] overflow-hidden bg-slate-100">
<div class="transform-wrapper absolute top-0 left-0 w-full h-auto origin-top-left"> <div class="transform-wrapper absolute top-0 left-0 w-full h-auto origin-top-left">
<img src="/assets/Europe_laea_location_map.svg" alt="Map of Europe" class="block w-full h-auto"> <img src="/assets/Europe.svg" alt="Map of Europe" class="block w-full h-auto">
<div class="points-container absolute top-0 left-0 w-full h-full"></div> <div class="points-container absolute top-0 left-0 w-full h-full"></div>
</div> </div>
<div class="absolute bottom-0 right-0 h-auto text-[0.6rem] bg-white px-0.5 bg-opacity-[0.5] border">
<i class="ri-creative-commons-line"></i>
<a href="https://commons.wikimedia.org/wiki/File:Europe_laea_topography.svg" target="_blank" class="">
Wikimedia Commons
</a>
</div>
<!-- Tooltip --> <!-- Tooltip -->
<div class="map-tooltip absolute bg-slate-800 text-white text-sm px-2 py-1 rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200 z-30 whitespace-nowrap" style="transform: translate(-50%, -100%); margin-top: -8px;"></div> <div class="map-tooltip absolute bg-slate-800 text-white text-sm px-2 py-1 rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200 z-30 whitespace-nowrap" style="transform: translate(-50%, -100%); margin-top: -8px;"></div>
</div> </div>
@@ -701,7 +707,9 @@ class K extends HTMLElement {
if (!this.places.length || !this.pointsContainer) if (!this.places.length || !this.pointsContainer)
return; return;
const e = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, t = { lon: 10, lat: 52 }, i = (s, o) => { const e = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, t = { lon: 10, lat: 52 }, i = (s, o) => {
const d = t.lon * Math.PI / 180, u = t.lat * Math.PI / 180, h = o * Math.PI / 180, p = s * Math.PI / 180, g = Math.sqrt(2 / (1 + Math.sin(u) * Math.sin(p) + Math.cos(u) * Math.cos(p) * Math.cos(h - d))), m = 6371e3 * g * Math.cos(p) * Math.sin(h - d), y = 6371e3 * g * (Math.cos(u) * Math.sin(p) - Math.sin(u) * Math.cos(p) * Math.cos(h - d)), w = m + 4321e3, v = y + 321e4, x = e.xmax - e.xmin, E = e.ymax - e.ymin, L = (w - e.xmin) / x * 100, C = (e.ymax - v) / E * 100; const d = t.lon * Math.PI / 180, u = t.lat * Math.PI / 180, h = o * Math.PI / 180, p = s * Math.PI / 180, b = Math.sqrt(
2 / (1 + Math.sin(u) * Math.sin(p) + Math.cos(u) * Math.cos(p) * Math.cos(h - d))
), g = 6371e3 * b * Math.cos(p) * Math.sin(h - d), y = 6371e3 * b * (Math.cos(u) * Math.sin(p) - Math.sin(u) * Math.cos(p) * Math.cos(h - d)), w = g + 4321e3, x = y + 321e4, v = e.xmax - e.xmin, E = e.ymax - e.ymin, L = (w - e.xmin) / v * 100, C = (e.ymax - x) / E * 100;
return { x: L, y: C }; return { x: L, y: C };
}, n = []; }, n = [];
this.places.forEach((s) => { this.places.forEach((s) => {
@@ -710,29 +718,30 @@ class K extends HTMLElement {
if (l.x >= 0 && l.x <= 100 && l.y >= 0 && l.y <= 100) { if (l.x >= 0 && l.x <= 100 && l.y >= 0 && l.y <= 100) {
n.push(l); n.push(l);
const c = document.createElement("div"); const c = document.createElement("div");
c.className = "map-point absolute w-1 h-1 bg-red-200 rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer hover:w-1.5 hover:h-1.5 hover:bg-red-400 hover:z-30", c.style.left = `${l.x}%`, c.style.top = `${l.y}%`, c.style.transformOrigin = "center"; c.className = "map-point hidden", c.style.left = `${l.x}%`, c.style.top = `${l.y}%`, c.style.transformOrigin = "center";
const d = `${s.name}${s.toponymName && s.toponymName !== s.name ? ` (${s.toponymName})` : ""}`; const d = `${s.name}${s.toponymName && s.toponymName !== s.name ? ` (${s.toponymName})` : ""}`;
c.dataset.placeId = s.id, c.dataset.tooltipText = d, c.addEventListener("mouseenter", (u) => this.showTooltip(u)), c.addEventListener("mouseleave", () => this.hideTooltip()), c.addEventListener("mousemove", (u) => this.updateTooltipPosition(u)), c.addEventListener("click", (u) => this.scrollToPlace(u)), this.pointsContainer.appendChild(c), this.mapPoints.set(s.id, c); c.dataset.placeId = s.id, c.dataset.tooltipText = d, c.addEventListener("mouseenter", (u) => this.showTooltip(u)), c.addEventListener("mouseleave", () => this.hideTooltip()), c.addEventListener("mousemove", (u) => this.updateTooltipPosition(u)), c.addEventListener("click", (u) => this.scrollToPlace(u)), this.pointsContainer.appendChild(c), this.mapPoints.set(s.id, c);
} }
} }
}), n.length > 0 && this.autoZoomToPoints(n); }), n.length > 0 && this.autoZoomToPoints(n);
} }
// Calculate bounding box of all points for the auto-zoom
autoZoomToPoints(e) { autoZoomToPoints(e) {
let t = 100, i = 0, n = 100, s = 0; let t = 100, i = 0, n = 100, s = 0;
e.forEach((b) => { e.forEach((f) => {
b.x < t && (t = b.x), b.x > i && (i = b.x), b.y < n && (n = b.y), b.y > s && (s = b.y); f.x < t && (t = f.x), f.x > i && (i = f.x), f.y < n && (n = f.y), f.y > s && (s = f.y);
}); });
const o = i - t, r = s - n, l = o * 0.05, c = r * 0.05, d = Math.max(0, t - l), u = Math.min(100, i + l), h = Math.max(0, n - c), p = Math.min(100, s + c), g = u - d, m = p - h, y = 5 / 7, w = g / m; const o = 0.06, r = i - t, l = s - n, c = r * o, d = l * o, u = Math.max(0, t - c), h = Math.min(100, i + c), p = Math.max(0, n - d), b = Math.min(100, s + d), g = h - u, y = b - p, w = 5 / 7, x = g / y;
let v = { x: d, y: h, width: g, height: m }; let v = { x: u, y: p, width: g, height: y };
if (w > y) { if (x > w) {
const b = g / y; const f = g / w;
v.y = h - (b - m) / 2, v.height = b; v.y = p - (f - y) / 2, v.height = f;
} else { } else {
const b = m * y; const f = y * w;
v.x = d - (b - g) / 2, v.width = b; v.x = u - (f - g) / 2, v.width = f;
} }
const x = 100 / v.width, E = -v.x, L = -v.y, C = `scale(${x}) translate(${E}%, ${L}%)`, P = this.querySelector(".transform-wrapper"); const E = 100 / v.width, L = -v.x, C = -v.y, N = `scale(${E}) translate(${L}%, ${C}%)`, P = this.querySelector(".transform-wrapper");
P && (P.style.transform = C); P && (P.style.transform = N);
} }
initializeScrollspy() { initializeScrollspy() {
const e = document.querySelectorAll("place-accordion[data-place-id]"); const e = document.querySelectorAll("place-accordion[data-place-id]");
@@ -764,16 +773,16 @@ class K extends HTMLElement {
e.className = "map-point absolute w-1.5 h-1.5 bg-red-500 border border-red-700 rounded-full shadow-md -translate-x-1/2 -translate-y-1/2 transition-all duration-300 opacity-100 saturate-100 z-20 cursor-pointer hover:w-2 hover:h-2 hover:bg-red-600 hover:z-30"; e.className = "map-point absolute w-1.5 h-1.5 bg-red-500 border border-red-700 rounded-full shadow-md -translate-x-1/2 -translate-y-1/2 transition-all duration-300 opacity-100 saturate-100 z-20 cursor-pointer hover:w-2 hover:h-2 hover:bg-red-600 hover:z-30";
} }
setPointInactive(e) { setPointInactive(e) {
e.className = "map-point absolute w-1 h-1 bg-red-200 rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer hover:w-1.5 hover:h-1.5 hover:bg-red-400 hover:z-30"; e.className = "map-point absolute w-[0.18rem] h-[0.18rem] bg-white opacity-[0.7] rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer hover:w-1.5 hover:h-1.5 hover:bg-red-400 hover:z-30 hover:opacity-[1.0]";
} }
showTooltip(e) { showTooltip(e) {
const i = e.target.dataset.tooltipText; const t = e.target, i = t.dataset.tooltipText, n = t.dataset.placeId;
this.tooltip && i && (this.tooltipTimeout && clearTimeout(this.tooltipTimeout), this.tooltip.textContent = i, this.updateTooltipPosition(e), this.tooltipTimeout = setTimeout(() => { this.isNewPopupBlocked(n) || this.tooltip && i && (this.tooltip.textContent = i, this.updateTooltipPosition(e), this.clearTimeouts(), this.tooltip.classList.remove("opacity-0"), this.tooltip.classList.add("opacity-100"), this.isTooltipVisible = !0);
this.tooltip.classList.remove("opacity-0"), this.tooltip.classList.add("opacity-100");
}, 500));
} }
hideTooltip() { hideTooltip() {
this.tooltipTimeout && (clearTimeout(this.tooltipTimeout), this.tooltipTimeout = null), this.tooltip && (this.tooltip.classList.remove("opacity-100"), this.tooltip.classList.add("opacity-0")); this.clearTimeouts(), this.hideTimeout = setTimeout(() => {
this.tooltip && (this.tooltip.classList.remove("opacity-100"), this.tooltip.classList.add("opacity-0"), this.isTooltipVisible = !1);
}, 150);
} }
updateTooltipPosition(e) { updateTooltipPosition(e) {
if (!this.tooltip) return; if (!this.tooltip) return;
@@ -798,31 +807,55 @@ class K extends HTMLElement {
}, 1e3)); }, 1e3));
} }
setupHeadingHoverListener() { setupHeadingHoverListener() {
document.addEventListener("place-heading-hover", this.handleHeadingHoverEvent.bind(this)); document.addEventListener("place-heading-hover", this.boundHandleHeadingHoverEvent);
}
clearTimeouts() {
this.showTimeout && (clearTimeout(this.showTimeout), this.showTimeout = null), this.hideTimeout && (clearTimeout(this.hideTimeout), this.hideTimeout = null), this.scrollBlockTimeout && (clearTimeout(this.scrollBlockTimeout), this.scrollBlockTimeout = null);
}
checkMousePositionAfterScroll() {
if (this.currentHoveredPlaceId) {
const e = new CustomEvent("place-heading-hover", {
detail: { placeId: this.currentHoveredPlaceId, action: "show" },
bubbles: !0
});
document.dispatchEvent(e);
}
}
checkExistingPopupAfterScroll() {
this.currentHoveredPlaceId !== this.placeIdBeforeScroll && this.tooltip && this.isTooltipVisible && (this.tooltip.classList.remove("opacity-100"), this.tooltip.classList.add("opacity-0"), this.isTooltipVisible = !1);
}
isScrollBlocked() {
return Date.now() - this.lastScrollTime < 300;
}
isNewPopupBlocked(e) {
return Date.now() - this.lastScrollTime < 300 ? e !== this.placeIdBeforeScroll : !1;
} }
handleHeadingHoverEvent(e) { handleHeadingHoverEvent(e) {
const { placeId: t, action: i } = e.detail, n = this.mapPoints.get(t); const { placeId: t, action: i } = e.detail, n = this.mapPoints.get(t);
if (n) if (n)
if (i === "show") { if (i === "show") {
n.classList.remove("w-1", "h-1", "w-1.5", "h-1.5"), n.classList.add("w-2", "h-2"), n.style.zIndex = "25"; if (this.currentHoveredPlaceId = t, n.classList.remove("w-1", "h-1", "w-1.5", "h-1.5"), n.classList.add("w-2", "h-2"), n.style.zIndex = "25", this.isNewPopupBlocked(t))
return;
const s = n.dataset.tooltipText; const s = n.dataset.tooltipText;
if (this.tooltip && s) { if (this.tooltip && s) {
this.tooltipTimeout && clearTimeout(this.tooltipTimeout), this.tooltip.textContent = s; this.tooltip.textContent = s;
const o = n.getBoundingClientRect(), r = this.mapElement.getBoundingClientRect(), l = o.left - r.left + o.width / 2, c = o.top - r.top + o.height / 2; const o = n.getBoundingClientRect(), r = this.mapElement.getBoundingClientRect(), l = o.left - r.left + o.width / 2, c = o.top - r.top + o.height / 2;
this.tooltip.style.left = `${l}px`, this.tooltip.style.top = `${c}px`, this.tooltipTimeout = setTimeout(() => { this.tooltip.style.left = `${l}px`, this.tooltip.style.top = `${c}px`, this.clearTimeouts(), this.isTooltipVisible ? (this.tooltip.classList.remove("opacity-0"), this.tooltip.classList.add("opacity-100")) : this.showTimeout = setTimeout(() => {
this.tooltip.classList.remove("opacity-0"), this.tooltip.classList.add("opacity-100"); this.tooltip.classList.remove("opacity-0"), this.tooltip.classList.add("opacity-100"), this.isTooltipVisible = !0;
}, 500); }, 150);
} }
} else i === "hide" && (this.tooltipTimeout && (clearTimeout(this.tooltipTimeout), this.tooltipTimeout = null), this.tooltip && (this.tooltip.classList.remove("opacity-100"), this.tooltip.classList.add("opacity-0")), n.classList.remove("w-2", "h-2"), n.className.includes("bg-red-500") ? n.classList.add("w-1.5", "h-1.5") : n.classList.add("w-1", "h-1"), n.style.zIndex = ""); } else i === "hide" && (this.currentHoveredPlaceId = "", this.clearTimeouts(), this.hideTimeout = setTimeout(() => {
this.tooltip && (this.tooltip.classList.remove("opacity-100"), this.tooltip.classList.add("opacity-0"), this.isTooltipVisible = !1);
}, 150), n.classList.remove("w-2", "h-2"), n.className.includes("bg-red-500") ? n.classList.add("w-1.5", "h-1.5") : n.classList.add("w-1", "h-1"), n.style.zIndex = "");
} }
disconnectedCallback() { disconnectedCallback() {
this.intersectionObserver && (this.intersectionObserver.disconnect(), this.intersectionObserver = null), this.tooltipTimeout && (clearTimeout(this.tooltipTimeout), this.tooltipTimeout = null), document.removeEventListener("place-heading-hover", this.handleHeadingHoverEvent.bind(this)); this.intersectionObserver && (this.intersectionObserver.disconnect(), this.intersectionObserver = null), this.clearTimeouts(), document.removeEventListener("place-heading-hover", this.boundHandleHeadingHoverEvent), window.removeEventListener("scroll", this.boundHandleScroll), document.removeEventListener("scroll", this.boundHandleScroll);
} }
} }
customElements.define("places-filter", D); customElements.define("places-filter", j);
customElements.define("place-accordion", j); customElements.define("place-accordion", K);
customElements.define("places-map", K); customElements.define("places-map", W);
class W extends HTMLElement { class Y extends HTMLElement {
constructor() { constructor() {
super(), this.searchInput = null, this.itemCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0; super(), this.searchInput = null, this.itemCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0;
} }
@@ -885,8 +918,8 @@ class W extends HTMLElement {
this.countElement && (t === "" ? this.countElement.style.display = "none" : (this.countElement.style.display = "", e === 0 ? this.countElement.textContent = "(0)" : this.countElement.textContent = `(${e})`)); this.countElement && (t === "" ? this.countElement.style.display = "none" : (this.countElement.style.display = "", e === 0 ? this.countElement.textContent = "(0)" : this.countElement.textContent = `(${e})`));
} }
} }
customElements.define("generic-filter", W); customElements.define("generic-filter", Y);
class Y extends HTMLElement { class Z extends HTMLElement {
constructor() { constructor() {
super(), this.resizeObserver = null; super(), this.resizeObserver = null;
} }
@@ -1019,15 +1052,15 @@ class Y extends HTMLElement {
if (l) if (l)
h = l; h = l;
else { else {
const g = this.getIssueContext(i); const b = this.getIssueContext(i);
h = g ? `${g}, ${i}` : `${i}`; h = b ? `${b}, ${i}` : `${i}`;
} }
if (d.innerHTML = h, s && i === s) { if (d.innerHTML = h, s && i === s) {
d.style.position = "relative"; d.style.position = "relative";
const g = d.querySelector(".target-page-dot"); const b = d.querySelector(".target-page-dot");
g && g.remove(); b && b.remove();
const m = document.createElement("span"); const g = document.createElement("span");
m.className = "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10", m.title = "verlinkte Seite", d.appendChild(m); g.className = "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10", g.title = "verlinkte Seite", d.appendChild(g);
} }
r ? r === "part-number" && o !== null ? u.innerHTML = `<span class="part-number bg-slate-100 text-slate-800 font-bold px-1.5 py-0.5 rounded border border-slate-400 flex items-center justify-center">${o}. Teil</span>` : u.innerHTML = this.generateIconFromType(r) : u.innerHTML = this.generateFallbackIcon(i, n, o), this.updateNavigationButtons(), this.style.display = "block", this.setAttribute("active", "true"); r ? r === "part-number" && o !== null ? u.innerHTML = `<span class="part-number bg-slate-100 text-slate-800 font-bold px-1.5 py-0.5 rounded border border-slate-400 flex items-center justify-center">${o}. Teil</span>` : u.innerHTML = this.generateIconFromType(r) : u.innerHTML = this.generateFallbackIcon(i, n, o), this.updateNavigationButtons(), this.style.display = "block", this.setAttribute("active", "true");
const p = this.querySelector(".flex-1.overflow-auto"); const p = this.querySelector(".flex-1.overflow-auto");
@@ -1239,7 +1272,7 @@ class Y extends HTMLElement {
return "KGPZ"; return "KGPZ";
} }
} }
customElements.define("single-page-viewer", Y); customElements.define("single-page-viewer", Z);
document.body.addEventListener("htmx:beforeRequest", function(a) { document.body.addEventListener("htmx:beforeRequest", function(a) {
const e = document.querySelector("single-page-viewer"); const e = document.querySelector("single-page-viewer");
e && e.style.display !== "none" && (console.log("Cleaning up single page viewer before HTMX navigation"), e.close()); e && e.style.display !== "none" && (console.log("Cleaning up single page viewer before HTMX navigation"), e.close());
@@ -1248,7 +1281,7 @@ window.addEventListener("beforeunload", function() {
const a = document.querySelector("single-page-viewer"); const a = document.querySelector("single-page-viewer");
a && a.close(); a && a.close();
}); });
class Z extends HTMLElement { class J extends HTMLElement {
constructor() { constructor() {
super(), this.isVisible = !1, this.scrollHandler = null, this.htmxAfterSwapHandler = null; super(), this.isVisible = !1, this.scrollHandler = null, this.htmxAfterSwapHandler = null;
} }
@@ -1289,8 +1322,8 @@ class Z extends HTMLElement {
}); });
} }
} }
customElements.define("scroll-to-top-button", Z); customElements.define("scroll-to-top-button", J);
class J extends HTMLElement { class X extends HTMLElement {
constructor() { constructor() {
super(), this.pageObserver = null, this.pageContainers = /* @__PURE__ */ new Map(), this.singlePageViewerActive = !1, this.singlePageViewerCurrentPage = null, this.boundHandleSinglePageViewer = this.handleSinglePageViewer.bind(this); super(), this.pageObserver = null, this.pageContainers = /* @__PURE__ */ new Map(), this.singlePageViewerActive = !1, this.singlePageViewerCurrentPage = null, this.boundHandleSinglePageViewer = this.handleSinglePageViewer.bind(this);
} }
@@ -1409,8 +1442,8 @@ class J extends HTMLElement {
this.pageObserver && (this.pageObserver.disconnect(), this.pageObserver = null), document.removeEventListener("singlepageviewer:opened", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:closed", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:pagechanged", this.boundHandleSinglePageViewer), this.pageContainers.clear(); this.pageObserver && (this.pageObserver.disconnect(), this.pageObserver = null), document.removeEventListener("singlepageviewer:opened", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:closed", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:pagechanged", this.boundHandleSinglePageViewer), this.pageContainers.clear();
} }
} }
customElements.define("inhaltsverzeichnis-scrollspy", J); customElements.define("inhaltsverzeichnis-scrollspy", X);
class X extends HTMLElement { class _ extends HTMLElement {
constructor() { constructor() {
super(), this.innerHTML = ` super(), this.innerHTML = `
<div id="error-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center backdrop-blur-sm"> <div id="error-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center backdrop-blur-sm">
@@ -1458,11 +1491,11 @@ class X extends HTMLElement {
window.showErrorModal = (e) => this.show(e), window.closeErrorModal = () => this.close(); window.showErrorModal = (e) => this.show(e), window.closeErrorModal = () => this.close();
} }
} }
customElements.define("error-modal", X); customElements.define("error-modal", _);
window.currentPageContainers = window.currentPageContainers || []; window.currentPageContainers = window.currentPageContainers || [];
window.currentActiveIndex = window.currentActiveIndex || 0; window.currentActiveIndex = window.currentActiveIndex || 0;
window.pageObserver = window.pageObserver || null; window.pageObserver = window.pageObserver || null;
function _(a, e, t, i = null) { function G(a, e, t, i = null) {
let n = document.querySelector("single-page-viewer"); let n = document.querySelector("single-page-viewer");
n || (n = document.createElement("single-page-viewer"), document.body.appendChild(n)); n || (n = document.createElement("single-page-viewer"), document.body.appendChild(n));
const s = a.closest('[data-beilage="true"]') !== null, o = window.templateData && window.templateData.targetPage ? window.templateData.targetPage : 0, r = a.closest(".newspaper-page-container, .piece-page-container"); const s = a.closest('[data-beilage="true"]') !== null, o = window.templateData && window.templateData.targetPage ? window.templateData.targetPage : 0, r = a.closest(".newspaper-page-container, .piece-page-container");
@@ -1472,15 +1505,15 @@ function _(a, e, t, i = null) {
const u = r.querySelector(".page-indicator"); const u = r.querySelector(".page-indicator");
if (u) { if (u) {
const h = u.cloneNode(!0); const h = u.cloneNode(!0);
h.querySelectorAll("i").forEach((m) => m.remove()), h.querySelectorAll('[class*="target-page-dot"], .target-page-indicator').forEach((m) => m.remove()), c = h.textContent.trim(); h.querySelectorAll("i").forEach((g) => g.remove()), h.querySelectorAll('[class*="target-page-dot"], .target-page-indicator').forEach((g) => g.remove()), c = h.textContent.trim();
} }
} }
n.show(a.src, a.alt, e, s, o, i, l, c); n.show(a.src, a.alt, e, s, o, i, l, c);
} }
function q() { function H() {
document.getElementById("pageModal").classList.add("hidden"); document.getElementById("pageModal").classList.add("hidden");
} }
function G() { function U() {
if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container")), window.currentActiveIndex = 0, S(), document.querySelector(".newspaper-page-container")) { if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container")), window.currentActiveIndex = 0, S(), document.querySelector(".newspaper-page-container")) {
let e = /* @__PURE__ */ new Set(); let e = /* @__PURE__ */ new Set();
window.pageObserver = new IntersectionObserver( window.pageObserver = new IntersectionObserver(
@@ -1501,7 +1534,7 @@ function G() {
}); });
} }
} }
function U() { function Q() {
if (window.currentActiveIndex > 0) { if (window.currentActiveIndex > 0) {
let a = -1; let a = -1;
const e = []; const e = [];
@@ -1522,7 +1555,7 @@ function U() {
}, 100)); }, 100));
} }
} }
function Q() { function ee() {
if (window.currentActiveIndex < window.currentPageContainers.length - 1) { if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
let a = -1; let a = -1;
const e = []; const e = [];
@@ -1543,8 +1576,8 @@ function Q() {
}, 100)); }, 100));
} }
} }
function ee() { function te() {
if (H()) { if (q()) {
const e = document.querySelector("#newspaper-content .newspaper-page-container"); const e = document.querySelector("#newspaper-content .newspaper-page-container");
e && e.scrollIntoView({ e && e.scrollIntoView({
block: "start" block: "start"
@@ -1563,7 +1596,7 @@ function ee() {
} }
} }
} }
function H() { function q() {
const a = []; const a = [];
window.currentPageContainers.forEach((e, t) => { window.currentPageContainers.forEach((e, t) => {
const i = e.getBoundingClientRect(), n = window.innerHeight, s = Math.max(i.top, 0), o = Math.min(i.bottom, n), r = Math.max(0, o - s), l = i.height; const i = e.getBoundingClientRect(), n = window.innerHeight, s = Math.max(i.top, 0), o = Math.min(i.bottom, n), r = Math.max(0, o - s), l = i.height;
@@ -1579,11 +1612,11 @@ function H() {
function S() { function S() {
const a = document.getElementById("prevPageBtn"), e = document.getElementById("nextPageBtn"), t = document.getElementById("beilageBtn"); const a = document.getElementById("prevPageBtn"), e = document.getElementById("nextPageBtn"), t = document.getElementById("beilageBtn");
if (a && (a.style.display = "flex", window.currentActiveIndex <= 0 ? (a.disabled = !0, a.classList.add("opacity-50", "cursor-not-allowed"), a.classList.remove("hover:bg-gray-200")) : (a.disabled = !1, a.classList.remove("opacity-50", "cursor-not-allowed"), a.classList.add("hover:bg-gray-200"))), e && (e.style.display = "flex", window.currentActiveIndex >= window.currentPageContainers.length - 1 ? (e.disabled = !0, e.classList.add("opacity-50", "cursor-not-allowed"), e.classList.remove("hover:bg-gray-200")) : (e.disabled = !1, e.classList.remove("opacity-50", "cursor-not-allowed"), e.classList.add("hover:bg-gray-200"))), t) { if (a && (a.style.display = "flex", window.currentActiveIndex <= 0 ? (a.disabled = !0, a.classList.add("opacity-50", "cursor-not-allowed"), a.classList.remove("hover:bg-gray-200")) : (a.disabled = !1, a.classList.remove("opacity-50", "cursor-not-allowed"), a.classList.add("hover:bg-gray-200"))), e && (e.style.display = "flex", window.currentActiveIndex >= window.currentPageContainers.length - 1 ? (e.disabled = !0, e.classList.add("opacity-50", "cursor-not-allowed"), e.classList.remove("hover:bg-gray-200")) : (e.disabled = !1, e.classList.remove("opacity-50", "cursor-not-allowed"), e.classList.add("hover:bg-gray-200"))), t) {
const i = H(), n = t.querySelector("i"); const i = q(), n = t.querySelector("i");
i ? (t.title = "Zur Hauptausgabe", t.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-800 border border-gray-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", n && (n.className = "ri-file-text-line text-lg lg:text-xl")) : (t.title = "Zu Beilage", t.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-amber-100 hover:bg-amber-200 text-amber-700 hover:text-amber-800 border border-amber-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", n && (n.className = "ri-attachment-line text-lg lg:text-xl")); i ? (t.title = "Zur Hauptausgabe", t.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-800 border border-gray-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", n && (n.className = "ri-file-text-line text-lg lg:text-xl")) : (t.title = "Zu Beilage", t.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-amber-100 hover:bg-amber-200 text-amber-700 hover:text-amber-800 border border-amber-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", n && (n.className = "ri-attachment-line text-lg lg:text-xl"));
} }
} }
function te() { function ie() {
const a = document.getElementById("shareLinkBtn"); const a = document.getElementById("shareLinkBtn");
let e = ""; let e = "";
if (window.currentActiveIndex !== void 0 && window.currentPageContainers && window.currentPageContainers[window.currentActiveIndex]) { if (window.currentActiveIndex !== void 0 && window.currentPageContainers && window.currentPageContainers[window.currentActiveIndex]) {
@@ -1595,54 +1628,54 @@ function te() {
title: document.title, title: document.title,
url: t url: t
}).catch((i) => { }).catch((i) => {
k(t, a); T(t, a);
}) : k(t, a); }) : T(t, a);
} }
function k(a, e) { function T(a, e) {
if (navigator.clipboard) if (navigator.clipboard)
navigator.clipboard.writeText(a).then(() => { navigator.clipboard.writeText(a).then(() => {
f(e, "Link kopiert!"); m(e, "Link kopiert!");
}).catch((t) => { }).catch((t) => {
f(e, "Kopieren fehlgeschlagen"); m(e, "Kopieren fehlgeschlagen");
}); });
else { else {
const t = document.createElement("textarea"); const t = document.createElement("textarea");
t.value = a, document.body.appendChild(t), t.select(); t.value = a, document.body.appendChild(t), t.select();
try { try {
const i = document.execCommand("copy"); const i = document.execCommand("copy");
f(e, i ? "Link kopiert!" : "Kopieren fehlgeschlagen"); m(e, i ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
f(e, "Kopieren fehlgeschlagen"); m(e, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(t); document.body.removeChild(t);
} }
} }
} }
function ie() { function ne() {
const a = document.getElementById("citationBtn"), e = document.title || "KGPZ"; const a = document.getElementById("citationBtn"), e = document.title || "KGPZ";
let t = window.location.origin + window.location.pathname; let t = window.location.origin + window.location.pathname;
t.includes("#") && (t = t.split("#")[0]); t.includes("#") && (t = t.split("#")[0]);
const i = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), n = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${e}. Digital verfügbar unter: ${t} (Zugriff: ${i}).`; const i = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), n = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${e}. Digital verfügbar unter: ${t} (Zugriff: ${i}).`;
if (navigator.clipboard) if (navigator.clipboard)
navigator.clipboard.writeText(n).then(() => { navigator.clipboard.writeText(n).then(() => {
f(a, "Zitation kopiert!"); m(a, "Zitation kopiert!");
}).catch((s) => { }).catch((s) => {
f(a, "Kopieren fehlgeschlagen"); m(a, "Kopieren fehlgeschlagen");
}); });
else { else {
const s = document.createElement("textarea"); const s = document.createElement("textarea");
s.value = n, document.body.appendChild(s), s.select(); s.value = n, document.body.appendChild(s), s.select();
try { try {
const o = document.execCommand("copy"); const o = document.execCommand("copy");
f(a, o ? "Zitation kopiert!" : "Kopieren fehlgeschlagen"); m(a, o ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
f(a, "Kopieren fehlgeschlagen"); m(a, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(s); document.body.removeChild(s);
} }
} }
} }
function f(a, e) { function m(a, e) {
const t = document.querySelector(".simple-popup"); const t = document.querySelector(".simple-popup");
t && t.remove(); t && t.remove();
const i = document.createElement("div"); const i = document.createElement("div");
@@ -1671,7 +1704,7 @@ function f(a, e) {
}, 200); }, 200);
}, 2e3); }, 2e3);
} }
function ne(a, e, t = !1) { function se(a, e, t = !1) {
let i = ""; let i = "";
if (t) if (t)
i = window.location.origin + window.location.pathname + `#beilage-1-page-${a}`; i = window.location.origin + window.location.pathname + `#beilage-1-page-${a}`;
@@ -1686,24 +1719,24 @@ function ne(a, e, t = !1) {
const n = i; const n = i;
if (navigator.clipboard) if (navigator.clipboard)
navigator.clipboard.writeText(n).then(() => { navigator.clipboard.writeText(n).then(() => {
f(e, "Link kopiert!"); m(e, "Link kopiert!");
}).catch((s) => { }).catch((s) => {
f(e, "Kopieren fehlgeschlagen"); m(e, "Kopieren fehlgeschlagen");
}); });
else { else {
const s = document.createElement("textarea"); const s = document.createElement("textarea");
s.value = n, document.body.appendChild(s), s.select(); s.value = n, document.body.appendChild(s), s.select();
try { try {
const o = document.execCommand("copy"); const o = document.execCommand("copy");
f(e, o ? "Link kopiert!" : "Kopieren fehlgeschlagen"); m(e, o ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
f(e, "Kopieren fehlgeschlagen"); m(e, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(s); document.body.removeChild(s);
} }
} }
} }
function se(a, e) { function oe(a, e) {
const t = document.title || "KGPZ", i = window.location.pathname.split("/"); const t = document.title || "KGPZ", i = window.location.pathname.split("/");
let n; let n;
if (i.length >= 3) { if (i.length >= 3) {
@@ -1714,30 +1747,30 @@ function se(a, e) {
const s = n, o = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), r = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${t}, Seite ${a}. Digital verfügbar unter: ${s} (Zugriff: ${o}).`; const s = n, o = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), r = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${t}, Seite ${a}. Digital verfügbar unter: ${s} (Zugriff: ${o}).`;
if (navigator.clipboard) if (navigator.clipboard)
navigator.clipboard.writeText(r).then(() => { navigator.clipboard.writeText(r).then(() => {
f(e, "Zitation kopiert!"); m(e, "Zitation kopiert!");
}).catch((l) => { }).catch((l) => {
f(e, "Kopieren fehlgeschlagen"); m(e, "Kopieren fehlgeschlagen");
}); });
else { else {
const l = document.createElement("textarea"); const l = document.createElement("textarea");
l.value = r, document.body.appendChild(l), l.select(); l.value = r, document.body.appendChild(l), l.select();
try { try {
const c = document.execCommand("copy"); const c = document.execCommand("copy");
f(e, c ? "Zitation kopiert!" : "Kopieren fehlgeschlagen"); m(e, c ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
f(e, "Kopieren fehlgeschlagen"); m(e, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(l); document.body.removeChild(l);
} }
} }
} }
function M() { function M() {
G(), window.addEventListener("scroll", function() { U(), window.addEventListener("scroll", function() {
clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => { clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => {
S(); S();
}, 50); }, 50);
}), document.addEventListener("keydown", function(a) { }), document.addEventListener("keydown", function(a) {
a.key === "Escape" && q(); a.key === "Escape" && H();
}); });
} }
function A() { function A() {
@@ -1769,21 +1802,21 @@ function B() {
"page-edition" "page-edition"
), a.includes("/akteure/") || a.includes("/autoren") ? e.classList.add("page-akteure") : a.match(/\/\d{4}\/\d+/) ? e.classList.add("page-ausgabe") : a.includes("/search") || a.includes("/suche") ? e.classList.add("page-search") : a.includes("/ort/") ? e.classList.add("page-ort") : a.includes("/kategorie/") ? e.classList.add("page-kategorie") : a.includes("/beitrag/") ? e.classList.add("page-piece") : a.includes("/edition") && e.classList.add("page-edition"); ), a.includes("/akteure/") || a.includes("/autoren") ? e.classList.add("page-akteure") : a.match(/\/\d{4}\/\d+/) ? e.classList.add("page-ausgabe") : a.includes("/search") || a.includes("/suche") ? e.classList.add("page-search") : a.includes("/ort/") ? e.classList.add("page-ort") : a.includes("/kategorie/") ? e.classList.add("page-kategorie") : a.includes("/beitrag/") ? e.classList.add("page-piece") : a.includes("/edition") && e.classList.add("page-edition");
} }
window.enlargePage = _; window.enlargePage = G;
window.closeModal = q; window.closeModal = H;
window.scrollToPreviousPage = U; window.scrollToPreviousPage = Q;
window.scrollToNextPage = Q; window.scrollToNextPage = ee;
window.scrollToBeilage = ee; window.scrollToBeilage = te;
window.shareCurrentPage = te; window.shareCurrentPage = ie;
window.generateCitation = ie; window.generateCitation = ne;
window.copyPagePermalink = ne; window.copyPagePermalink = se;
window.generatePageCitation = se; window.generatePageCitation = oe;
B(); B();
A(); A();
document.querySelector(".newspaper-page-container") && M(); document.querySelector(".newspaper-page-container") && M();
let oe = function(a) { let re = function(a) {
B(), A(), I(), setTimeout(() => { B(), A(), I(), setTimeout(() => {
document.querySelector(".newspaper-page-container") && M(); document.querySelector(".newspaper-page-container") && M();
}, 50); }, 50);
}; };
document.body.addEventListener("htmx:afterSettle", oe); document.body.addEventListener("htmx:afterSettle", re);

File diff suppressed because one or more lines are too long

731
views/public/Europe.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@@ -40,23 +40,23 @@ export class PlacesFilter extends HTMLElement {
} }
setupEventListeners() { setupEventListeners() {
this.searchInput = this.querySelector('#places-search'); this.searchInput = this.querySelector("#places-search");
if (this.searchInput) { if (this.searchInput) {
this.searchInput.addEventListener('input', this.handleSearchInput.bind(this)); this.searchInput.addEventListener("input", this.handleSearchInput.bind(this));
} }
} }
cleanupEventListeners() { cleanupEventListeners() {
if (this.searchInput) { if (this.searchInput) {
this.searchInput.removeEventListener('input', this.handleSearchInput.bind(this)); this.searchInput.removeEventListener("input", this.handleSearchInput.bind(this));
} }
} }
initializePlaceCards() { initializePlaceCards() {
// Find all place cards and the count element // Find all place cards and the count element
const container = this.closest('.bg-white') || document; const container = this.closest(".bg-white") || document;
this.placeCards = Array.from(container.querySelectorAll('[data-place-name]')); this.placeCards = Array.from(container.querySelectorAll("[data-place-name]"));
this.countElement = container.querySelector('[data-places-count]'); this.countElement = container.querySelector("[data-places-count]");
if (this.countElement) { if (this.countElement) {
this.originalCount = this.placeCards.length; this.originalCount = this.placeCards.length;
@@ -81,20 +81,21 @@ export class PlacesFilter extends HTMLElement {
const normalizedSearch = searchTerm.toLowerCase(); const normalizedSearch = searchTerm.toLowerCase();
let visibleCount = 0; let visibleCount = 0;
this.placeCards.forEach(card => { this.placeCards.forEach((card) => {
const placeName = card.getAttribute('data-place-name')?.toLowerCase() || ''; const placeName = card.getAttribute("data-place-name")?.toLowerCase() || "";
const modernName = card.getAttribute('data-modern-name')?.toLowerCase() || ''; const modernName = card.getAttribute("data-modern-name")?.toLowerCase() || "";
// Check if search term matches either the place name or modern name // Check if search term matches either the place name or modern name
const isMatch = searchTerm === '' || const isMatch =
searchTerm === "" ||
placeName.includes(normalizedSearch) || placeName.includes(normalizedSearch) ||
modernName.includes(normalizedSearch); modernName.includes(normalizedSearch);
if (isMatch) { if (isMatch) {
card.style.display = ''; card.style.display = "";
visibleCount++; visibleCount++;
} else { } else {
card.style.display = 'none'; card.style.display = "none";
} }
}); });
@@ -105,7 +106,7 @@ export class PlacesFilter extends HTMLElement {
updateCountDisplay(visibleCount, searchTerm) { updateCountDisplay(visibleCount, searchTerm) {
if (!this.countElement) return; if (!this.countElement) return;
if (searchTerm === '') { if (searchTerm === "") {
// Show original count when no search // Show original count when no search
this.countElement.textContent = `Alle Orte (${this.originalCount})`; this.countElement.textContent = `Alle Orte (${this.originalCount})`;
} else if (visibleCount === 0) { } else if (visibleCount === 0) {
@@ -128,6 +129,12 @@ export class PlaceAccordion extends HTMLElement {
this.isExpanded = false; this.isExpanded = false;
this.isLoading = false; this.isLoading = false;
this.hasLoaded = false; this.hasLoaded = false;
// Bind event handlers to maintain consistent references
this.boundHandleClick = this.handleClick.bind(this);
this.boundHandleMapClick = this.handleMapClick.bind(this);
this.boundHandleHeadingHover = this.handleHeadingHover.bind(this);
this.boundHandleHeadingLeave = this.handleHeadingLeave.bind(this);
} }
connectedCallback() { connectedCallback() {
@@ -145,18 +152,19 @@ export class PlaceAccordion extends HTMLElement {
cleanupMapEventListeners() { cleanupMapEventListeners() {
// Clean up map event listeners // Clean up map event listeners
document.removeEventListener('place-map-clicked', this.handleMapClick.bind(this)); document.removeEventListener("place-map-clicked", this.boundHandleMapClick);
// Clean up hover event listeners // Clean up hover event listeners
this.removeEventListener('mouseenter', this.handleHeadingHover.bind(this)); this.removeEventListener("mouseenter", this.boundHandleHeadingHover);
this.removeEventListener('mouseleave', this.handleHeadingLeave.bind(this)); this.removeEventListener("mouseleave", this.boundHandleHeadingLeave);
} }
setupAccordion() { setupAccordion() {
// Add chevron icon if not already present // Add chevron icon if not already present
if (!this.querySelector('.accordion-chevron')) { if (!this.querySelector(".accordion-chevron")) {
const chevron = document.createElement('i'); const chevron = document.createElement("i");
chevron.className = 'ri-chevron-down-line accordion-chevron transition-transform duration-200 text-slate-400'; chevron.className =
"ri-chevron-down-line accordion-chevron transition-transform duration-200 text-slate-400";
// Find the badge and insert chevron before it // Find the badge and insert chevron before it
const badge = this.querySelector('[class*="bg-slate-100"]'); const badge = this.querySelector('[class*="bg-slate-100"]');
@@ -166,19 +174,20 @@ export class PlaceAccordion extends HTMLElement {
} }
// Create content container if not exists // Create content container if not exists
if (!this.querySelector('[data-content]')) { if (!this.querySelector("[data-content]")) {
const placeId = this.getAttribute('data-place-id'); const placeId = this.getAttribute("data-place-id");
const contentContainer = document.createElement('div'); const contentContainer = document.createElement("div");
contentContainer.setAttribute('data-content', ''); contentContainer.setAttribute("data-content", "");
contentContainer.className = 'accordion-content overflow-hidden transition-all duration-300 max-h-0'; contentContainer.className =
"accordion-content overflow-hidden transition-all duration-300 max-h-0";
// Add HTMX attributes to override body defaults // Add HTMX attributes to override body defaults
contentContainer.setAttribute('hx-get', `/ort/fragment/${placeId}`); contentContainer.setAttribute("hx-get", `/ort/fragment/${placeId}`);
contentContainer.setAttribute('hx-trigger', 'load-content'); contentContainer.setAttribute("hx-trigger", "load-content");
contentContainer.setAttribute('hx-swap', 'innerHTML'); contentContainer.setAttribute("hx-swap", "innerHTML");
contentContainer.setAttribute('hx-target', 'this'); contentContainer.setAttribute("hx-target", "this");
contentContainer.setAttribute('hx-select', '.place-fragment-content'); contentContainer.setAttribute("hx-select", ".place-fragment-content");
contentContainer.setAttribute('hx-boost', 'false'); // Override body's hx-boost="true" contentContainer.setAttribute("hx-boost", "false"); // Override body's hx-boost="true"
this.appendChild(contentContainer); this.appendChild(contentContainer);
} }
@@ -186,43 +195,43 @@ export class PlaceAccordion extends HTMLElement {
setupEventListeners() { setupEventListeners() {
// Add click listener to the entire component // Add click listener to the entire component
this.addEventListener('click', this.handleClick.bind(this)); this.addEventListener("click", this.boundHandleClick);
} }
cleanupEventListeners() { cleanupEventListeners() {
this.removeEventListener('click', this.handleClick.bind(this)); this.removeEventListener("click", this.boundHandleClick);
} }
setupMapEventListeners() { setupMapEventListeners() {
// Listen for map click events // Listen for map click events
document.addEventListener('place-map-clicked', this.handleMapClick.bind(this)); document.addEventListener("place-map-clicked", this.boundHandleMapClick);
} }
setupHoverEvents() { setupHoverEvents() {
// Add hover listeners to the entire accordion element (including expanded content) // Add hover listeners to the entire accordion element (including expanded content)
this.addEventListener('mouseenter', this.handleHeadingHover.bind(this)); this.addEventListener("mouseenter", this.boundHandleHeadingHover);
this.addEventListener('mouseleave', this.handleHeadingLeave.bind(this)); this.addEventListener("mouseleave", this.boundHandleHeadingLeave);
} }
handleHeadingHover() { handleHeadingHover() {
const placeId = this.getAttribute('data-place-id'); const placeId = this.getAttribute("data-place-id");
if (placeId) { if (placeId) {
// Emit event to show tooltip on map // Emit event to show tooltip on map
const showTooltipEvent = new CustomEvent('place-heading-hover', { const showTooltipEvent = new CustomEvent("place-heading-hover", {
detail: { placeId, action: 'show' }, detail: { placeId, action: "show" },
bubbles: true bubbles: true,
}); });
document.dispatchEvent(showTooltipEvent); document.dispatchEvent(showTooltipEvent);
} }
} }
handleHeadingLeave() { handleHeadingLeave() {
const placeId = this.getAttribute('data-place-id'); const placeId = this.getAttribute("data-place-id");
if (placeId) { if (placeId) {
// Emit event to hide tooltip on map // Emit event to hide tooltip on map
const hideTooltipEvent = new CustomEvent('place-heading-hover', { const hideTooltipEvent = new CustomEvent("place-heading-hover", {
detail: { placeId, action: 'hide' }, detail: { placeId, action: "hide" },
bubbles: true bubbles: true,
}); });
document.dispatchEvent(hideTooltipEvent); document.dispatchEvent(hideTooltipEvent);
} }
@@ -230,7 +239,7 @@ export class PlaceAccordion extends HTMLElement {
handleMapClick(event) { handleMapClick(event) {
const clickedPlaceId = event.detail.placeId; const clickedPlaceId = event.detail.placeId;
const myPlaceId = this.getAttribute('data-place-id'); const myPlaceId = this.getAttribute("data-place-id");
// If this accordion matches the clicked place, expand it // If this accordion matches the clicked place, expand it
if (clickedPlaceId === myPlaceId && !this.isExpanded) { if (clickedPlaceId === myPlaceId && !this.isExpanded) {
@@ -243,7 +252,7 @@ export class PlaceAccordion extends HTMLElement {
handleClick(event) { handleClick(event) {
// Only handle clicks on the place name area, not on expanded content // Only handle clicks on the place name area, not on expanded content
const contentContainer = this.querySelector('[data-content]'); const contentContainer = this.querySelector("[data-content]");
if (contentContainer && contentContainer.contains(event.target)) { if (contentContainer && contentContainer.contains(event.target)) {
return; // Don't toggle if clicking inside expanded content return; // Don't toggle if clicking inside expanded content
} }
@@ -266,7 +275,7 @@ export class PlaceAccordion extends HTMLElement {
this.updateChevron(); this.updateChevron();
this.updateBorders(); this.updateBorders();
const contentContainer = this.querySelector('[data-content]'); const contentContainer = this.querySelector("[data-content]");
if (!contentContainer) return; if (!contentContainer) return;
// Load content if not already loaded // Load content if not already loaded
@@ -274,7 +283,7 @@ export class PlaceAccordion extends HTMLElement {
this.loadContent(); this.loadContent();
} else { } else {
// Just show existing content // Just show existing content
contentContainer.style.maxHeight = contentContainer.scrollHeight + 'px'; contentContainer.style.maxHeight = contentContainer.scrollHeight + "px";
} }
} }
@@ -283,19 +292,19 @@ export class PlaceAccordion extends HTMLElement {
this.updateChevron(); this.updateChevron();
this.updateBorders(); this.updateBorders();
const contentContainer = this.querySelector('[data-content]'); const contentContainer = this.querySelector("[data-content]");
if (contentContainer) { if (contentContainer) {
contentContainer.style.maxHeight = '0px'; contentContainer.style.maxHeight = "0px";
} }
} }
loadContent() { loadContent() {
this.isLoading = true; this.isLoading = true;
const contentContainer = this.querySelector('[data-content]'); const contentContainer = this.querySelector("[data-content]");
// Show loading state // Show loading state
contentContainer.innerHTML = '<div class="p-4 text-center text-slate-500">Lädt...</div>'; contentContainer.innerHTML = '<div class="p-4 text-center text-slate-500">Lädt...</div>';
contentContainer.style.maxHeight = contentContainer.scrollHeight + 'px'; contentContainer.style.maxHeight = contentContainer.scrollHeight + "px";
// Set up event listeners for HTMX events // Set up event listeners for HTMX events
const handleAfterRequest = () => { const handleAfterRequest = () => {
@@ -303,31 +312,32 @@ export class PlaceAccordion extends HTMLElement {
this.isLoading = false; this.isLoading = false;
// Adjust height after content loads // Adjust height after content loads
setTimeout(() => { setTimeout(() => {
contentContainer.style.maxHeight = contentContainer.scrollHeight + 'px'; contentContainer.style.maxHeight = contentContainer.scrollHeight + "px";
}, 10); }, 10);
contentContainer.removeEventListener('htmx:afterRequest', handleAfterRequest); contentContainer.removeEventListener("htmx:afterRequest", handleAfterRequest);
}; };
const handleResponseError = () => { const handleResponseError = () => {
this.isLoading = false; this.isLoading = false;
contentContainer.innerHTML = '<div class="p-4 text-center text-red-500">Fehler beim Laden</div>'; contentContainer.innerHTML =
contentContainer.removeEventListener('htmx:responseError', handleResponseError); '<div class="p-4 text-center text-red-500">Fehler beim Laden</div>';
contentContainer.removeEventListener("htmx:responseError", handleResponseError);
}; };
contentContainer.addEventListener('htmx:afterRequest', handleAfterRequest); contentContainer.addEventListener("htmx:afterRequest", handleAfterRequest);
contentContainer.addEventListener('htmx:responseError', handleResponseError); contentContainer.addEventListener("htmx:responseError", handleResponseError);
// Trigger the HTMX request // Trigger the HTMX request
htmx.trigger(contentContainer, 'load-content'); htmx.trigger(contentContainer, "load-content");
} }
updateChevron() { updateChevron() {
const chevron = this.querySelector('.accordion-chevron'); const chevron = this.querySelector(".accordion-chevron");
if (chevron) { if (chevron) {
if (this.isExpanded) { if (this.isExpanded) {
chevron.style.transform = 'rotate(180deg)'; chevron.style.transform = "rotate(180deg)";
} else { } else {
chevron.style.transform = 'rotate(0deg)'; chevron.style.transform = "rotate(0deg)";
} }
} }
} }
@@ -335,16 +345,16 @@ export class PlaceAccordion extends HTMLElement {
updateBorders() { updateBorders() {
if (this.isExpanded) { if (this.isExpanded) {
// When expanded: remove border from header, add border to whole component // When expanded: remove border from header, add border to whole component
this.classList.add('border-b', 'border-slate-100'); this.classList.add("border-b", "border-slate-100");
} else { } else {
// When collapsed: add border to component (for separation between items) // When collapsed: add border to component (for separation between items)
this.classList.add('border-b', 'border-slate-100'); this.classList.add("border-b", "border-slate-100");
} }
// Remove border from last item if it's the last child // Remove border from last item if it's the last child
const isLastChild = !this.nextElementSibling; const isLastChild = !this.nextElementSibling;
if (isLastChild) { if (isLastChild) {
this.classList.remove('border-b'); this.classList.remove("border-b");
} }
} }
} }
@@ -362,7 +372,17 @@ export class PlacesMap extends HTMLElement {
this.intersectionObserver = null; this.intersectionObserver = null;
this.mapPoints = new Map(); // Map of placeId -> point element this.mapPoints = new Map(); // Map of placeId -> point element
this.tooltip = null; this.tooltip = null;
this.tooltipTimeout = null;
// New tooltip system properties
this.showTimeout = null;
this.hideTimeout = null;
this.isTooltipVisible = false;
// Simple place ID tracking
this.currentHoveredPlaceId = "";
// Bind event handlers to maintain consistent references
this.boundHandleHeadingHoverEvent = this.handleHeadingHoverEvent.bind(this);
} }
connectedCallback() { connectedCallback() {
@@ -383,7 +403,7 @@ export class PlacesMap extends HTMLElement {
this.places = JSON.parse(placesData); this.places = JSON.parse(placesData);
} }
} catch (error) { } catch (error) {
console.error('Failed to parse places data:', error); console.error("Failed to parse places data:", error);
this.places = []; this.places = [];
} }
} }
@@ -392,17 +412,23 @@ export class PlacesMap extends HTMLElement {
this.innerHTML = ` this.innerHTML = `
<div class="map-container relative w-full aspect-[5/7] overflow-hidden bg-slate-100"> <div class="map-container relative w-full aspect-[5/7] overflow-hidden bg-slate-100">
<div class="transform-wrapper absolute top-0 left-0 w-full h-auto origin-top-left"> <div class="transform-wrapper absolute top-0 left-0 w-full h-auto origin-top-left">
<img src="/assets/Europe_laea_location_map.svg" alt="Map of Europe" class="block w-full h-auto"> <img src="/assets/Europe.svg" alt="Map of Europe" class="block w-full h-auto">
<div class="points-container absolute top-0 left-0 w-full h-full"></div> <div class="points-container absolute top-0 left-0 w-full h-full"></div>
</div> </div>
<div class="absolute bottom-0 right-0 h-auto text-[0.6rem] bg-white px-0.5 bg-opacity-[0.5] border">
<i class="ri-creative-commons-line"></i>
<a href="https://commons.wikimedia.org/wiki/File:Europe_laea_topography.svg" target="_blank" class="">
Wikimedia Commons
</a>
</div>
<!-- Tooltip --> <!-- Tooltip -->
<div class="map-tooltip absolute bg-slate-800 text-white text-sm px-2 py-1 rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200 z-30 whitespace-nowrap" style="transform: translate(-50%, -100%); margin-top: -8px;"></div> <div class="map-tooltip absolute bg-slate-800 text-white text-sm px-2 py-1 rounded shadow-lg pointer-events-none opacity-0 transition-opacity duration-200 z-30 whitespace-nowrap" style="transform: translate(-50%, -100%); margin-top: -8px;"></div>
</div> </div>
`; `;
this.mapElement = this.querySelector('.map-container'); this.mapElement = this.querySelector(".map-container");
this.pointsContainer = this.querySelector('.points-container'); this.pointsContainer = this.querySelector(".points-container");
this.tooltip = this.querySelector('.map-tooltip'); this.tooltip = this.querySelector(".map-tooltip");
} }
initializeMap() { initializeMap() {
@@ -410,34 +436,43 @@ export class PlacesMap extends HTMLElement {
return; return;
} }
// Map extent constants (same as example) // Map extent constants
const MAP_EXTENT_METERS = { xmin: 2555000, ymin: 1350000, xmax: 7405000, ymax: 5500000 }; const MAP_EXTENT_METERS = { xmin: 2555000, ymin: 1350000, xmax: 7405000, ymax: 5500000 };
const PROJECTION_CENTER = { lon: 10, lat: 52 }; const PROJECTION_CENTER = { lon: 10, lat: 52 };
// Convert lat/lng to percent position // Convert lat/lng to % calculation
const convertLatLngToPercent = (lat, lng) => { const convertLatLngToPercent = (lat, lng) => {
const R = 6371000; const R = 6371000;
const FE = 4321000; const FE = 4321000;
const FN = 3210000; const FN = 3210000;
const lon_0_rad = PROJECTION_CENTER.lon * Math.PI / 180; const lon_0_rad = (PROJECTION_CENTER.lon * Math.PI) / 180;
const lat_0_rad = PROJECTION_CENTER.lat * Math.PI / 180; const lat_0_rad = (PROJECTION_CENTER.lat * Math.PI) / 180;
const lon_rad = lng * Math.PI / 180; const lon_rad = (lng * Math.PI) / 180;
const lat_rad = lat * Math.PI / 180; const lat_rad = (lat * Math.PI) / 180;
const k_prime = Math.sqrt(2 / (1 + Math.sin(lat_0_rad) * Math.sin(lat_rad) + Math.cos(lat_0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon_0_rad))); const k_prime = Math.sqrt(
2 /
(1 +
Math.sin(lat_0_rad) * Math.sin(lat_rad) +
Math.cos(lat_0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon_0_rad)),
);
const x_proj = R * k_prime * Math.cos(lat_rad) * Math.sin(lon_rad - lon_0_rad); const x_proj = R * k_prime * Math.cos(lat_rad) * Math.sin(lon_rad - lon_0_rad);
const y_proj = R * k_prime * (Math.cos(lat_0_rad) * Math.sin(lat_rad) - Math.sin(lat_0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon_0_rad)); const y_proj =
R *
k_prime *
(Math.cos(lat_0_rad) * Math.sin(lat_rad) -
Math.sin(lat_0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon_0_rad));
const finalX = x_proj + FE; const finalX = x_proj + FE;
const finalY = y_proj + FN; const finalY = y_proj + FN;
const mapWidthMeters = MAP_EXTENT_METERS.xmax - MAP_EXTENT_METERS.xmin; const mapWidthMeters = MAP_EXTENT_METERS.xmax - MAP_EXTENT_METERS.xmin;
const mapHeightMeters = MAP_EXTENT_METERS.ymax - MAP_EXTENT_METERS.ymin; const mapHeightMeters = MAP_EXTENT_METERS.ymax - MAP_EXTENT_METERS.ymin;
const xPercent = (finalX - MAP_EXTENT_METERS.xmin) / mapWidthMeters * 100; const xPercent = ((finalX - MAP_EXTENT_METERS.xmin) / mapWidthMeters) * 100;
const yPercent = (MAP_EXTENT_METERS.ymax - finalY) / mapHeightMeters * 100; const yPercent = ((MAP_EXTENT_METERS.ymax - finalY) / mapHeightMeters) * 100;
return { x: xPercent, y: yPercent }; return { x: xPercent, y: yPercent };
}; };
// Create points and track positions // Create points and track positions
const pointPositions = []; const pointPositions = [];
this.places.forEach(place => { this.places.forEach((place) => {
if (place.lat && place.lng) { if (place.lat && place.lng) {
const lat = parseFloat(place.lat); const lat = parseFloat(place.lat);
const lng = parseFloat(place.lng); const lng = parseFloat(place.lng);
@@ -447,20 +482,20 @@ export class PlacesMap extends HTMLElement {
if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) { if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) {
pointPositions.push(position); pointPositions.push(position);
const point = document.createElement('div'); const point = document.createElement("div");
point.className = 'map-point absolute w-1 h-1 bg-red-200 rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer hover:w-1.5 hover:h-1.5 hover:bg-red-400 hover:z-30'; point.className = "map-point hidden";
point.style.left = `${position.x}%`; point.style.left = `${position.x}%`;
point.style.top = `${position.y}%`; point.style.top = `${position.y}%`;
point.style.transformOrigin = 'center'; point.style.transformOrigin = "center";
const tooltipText = `${place.name}${place.toponymName && place.toponymName !== place.name ? ` (${place.toponymName})` : ''}`; const tooltipText = `${place.name}${place.toponymName && place.toponymName !== place.name ? ` (${place.toponymName})` : ""}`;
point.dataset.placeId = place.id; point.dataset.placeId = place.id;
point.dataset.tooltipText = tooltipText; point.dataset.tooltipText = tooltipText;
// Add hover and click event listeners // Add hover and click event listeners
point.addEventListener('mouseenter', (e) => this.showTooltip(e)); point.addEventListener("mouseenter", (e) => this.showTooltip(e));
point.addEventListener('mouseleave', () => this.hideTooltip()); point.addEventListener("mouseleave", () => this.hideTooltip());
point.addEventListener('mousemove', (e) => this.updateTooltipPosition(e)); point.addEventListener("mousemove", (e) => this.updateTooltipPosition(e));
point.addEventListener('click', (e) => this.scrollToPlace(e)); point.addEventListener("click", (e) => this.scrollToPlace(e));
this.pointsContainer.appendChild(point); this.pointsContainer.appendChild(point);
@@ -470,27 +505,31 @@ export class PlacesMap extends HTMLElement {
} }
}); });
// Auto-zoom to fit all points // Auto-zoom
if (pointPositions.length > 0) { if (pointPositions.length > 0) {
this.autoZoomToPoints(pointPositions); this.autoZoomToPoints(pointPositions);
} }
} }
// Calculate bounding box of all points for the auto-zoom
autoZoomToPoints(pointPositions) { autoZoomToPoints(pointPositions) {
// Calculate bounding box of all points let minX = 100,
let minX = 100, maxX = 0, minY = 100, maxY = 0; maxX = 0,
pointPositions.forEach(pos => { minY = 100,
maxY = 0;
pointPositions.forEach((pos) => {
if (pos.x < minX) minX = pos.x; if (pos.x < minX) minX = pos.x;
if (pos.x > maxX) maxX = pos.x; if (pos.x > maxX) maxX = pos.x;
if (pos.y < minY) minY = pos.y; if (pos.y < minY) minY = pos.y;
if (pos.y > maxY) maxY = pos.y; if (pos.y > maxY) maxY = pos.y;
}); });
// Add 5% padding // 5% padding
const PADDING = 0.06;
const width = maxX - minX; const width = maxX - minX;
const height = maxY - minY; const height = maxY - minY;
const paddingX = width * 0.05; const paddingX = width * PADDING;
const paddingY = height * 0.05; const paddingY = height * PADDING;
const paddedMinX = Math.max(0, minX - paddingX); const paddedMinX = Math.max(0, minX - paddingX);
const paddedMaxX = Math.min(100, maxX + paddingX); const paddedMaxX = Math.min(100, maxX + paddingX);
@@ -501,17 +540,17 @@ export class PlacesMap extends HTMLElement {
const newHeight = paddedMaxY - paddedMinY; const newHeight = paddedMaxY - paddedMinY;
// Maintain 5:7 aspect ratio // Maintain 5:7 aspect ratio
const targetAspectRatio = 5 / 7; const ASPECT_RATIO = 5 / 7;
const pointsAspectRatio = newWidth / newHeight; const pointsAspectRatio = newWidth / newHeight;
let finalViewBox = { x: paddedMinX, y: paddedMinY, width: newWidth, height: newHeight }; let finalViewBox = { x: paddedMinX, y: paddedMinY, width: newWidth, height: newHeight };
if (pointsAspectRatio > targetAspectRatio) { if (pointsAspectRatio > ASPECT_RATIO) {
const newTargetHeight = newWidth / targetAspectRatio; const newTargetHeight = newWidth / ASPECT_RATIO;
finalViewBox.y = paddedMinY - (newTargetHeight - newHeight) / 2; finalViewBox.y = paddedMinY - (newTargetHeight - newHeight) / 2;
finalViewBox.height = newTargetHeight; finalViewBox.height = newTargetHeight;
} else { } else {
const newTargetWidth = newHeight * targetAspectRatio; const newTargetWidth = newHeight * ASPECT_RATIO;
finalViewBox.x = paddedMinX - (newTargetWidth - newWidth) / 2; finalViewBox.x = paddedMinX - (newTargetWidth - newWidth) / 2;
finalViewBox.width = newTargetWidth; finalViewBox.width = newTargetWidth;
} }
@@ -522,7 +561,7 @@ export class PlacesMap extends HTMLElement {
const translateY = -finalViewBox.y; const translateY = -finalViewBox.y;
const transformValue = `scale(${scale}) translate(${translateX}%, ${translateY}%)`; const transformValue = `scale(${scale}) translate(${translateX}%, ${translateY}%)`;
const transformWrapper = this.querySelector('.transform-wrapper'); const transformWrapper = this.querySelector(".transform-wrapper");
if (transformWrapper) { if (transformWrapper) {
transformWrapper.style.transform = transformValue; transformWrapper.style.transform = transformValue;
} }
@@ -530,19 +569,19 @@ export class PlacesMap extends HTMLElement {
initializeScrollspy() { initializeScrollspy() {
// Find all place containers in the places list // Find all place containers in the places list
const placeContainers = document.querySelectorAll('place-accordion[data-place-id]'); const placeContainers = document.querySelectorAll("place-accordion[data-place-id]");
if (!placeContainers.length) return; if (!placeContainers.length) return;
// First, ensure all points start in inactive state // First, ensure all points start in inactive state
this.mapPoints.forEach(point => { this.mapPoints.forEach((point) => {
this.setPointInactive(point); this.setPointInactive(point);
}); });
// Create intersection observer // Create intersection observer
this.intersectionObserver = new IntersectionObserver( this.intersectionObserver = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach(entry => { entries.forEach((entry) => {
const placeId = entry.target.getAttribute('data-place-id'); const placeId = entry.target.getAttribute("data-place-id");
const mapPoint = this.mapPoints.get(placeId); const mapPoint = this.mapPoints.get(placeId);
if (mapPoint) { if (mapPoint) {
@@ -560,22 +599,22 @@ export class PlacesMap extends HTMLElement {
// Trigger when element enters viewport // Trigger when element enters viewport
threshold: 0.1, threshold: 0.1,
// No root margin for precise detection // No root margin for precise detection
rootMargin: '0px' rootMargin: "0px",
} },
); );
// Observe all place containers // Observe all place containers
placeContainers.forEach(container => { placeContainers.forEach((container) => {
this.intersectionObserver.observe(container); this.intersectionObserver.observe(container);
}); });
// Force an immediate check by triggering a scroll event // Force an immediate check by triggering a scroll event
setTimeout(() => { setTimeout(() => {
// Manually trigger intersection calculation // Manually trigger intersection calculation
placeContainers.forEach(container => { placeContainers.forEach((container) => {
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0; const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
const placeId = container.getAttribute('data-place-id'); const placeId = container.getAttribute("data-place-id");
const mapPoint = this.mapPoints.get(placeId); const mapPoint = this.mapPoints.get(placeId);
if (mapPoint && isVisible) { if (mapPoint && isVisible) {
@@ -587,47 +626,50 @@ export class PlacesMap extends HTMLElement {
setPointActive(point) { setPointActive(point) {
// Active state: larger, full color, full opacity, higher z-index // Active state: larger, full color, full opacity, higher z-index
point.className = 'map-point absolute w-1.5 h-1.5 bg-red-500 border border-red-700 rounded-full shadow-md -translate-x-1/2 -translate-y-1/2 transition-all duration-300 opacity-100 saturate-100 z-20 cursor-pointer hover:w-2 hover:h-2 hover:bg-red-600 hover:z-30'; point.className =
"map-point absolute w-1.5 h-1.5 bg-red-500 border border-red-700 rounded-full shadow-md -translate-x-1/2 -translate-y-1/2 transition-all duration-300 opacity-100 saturate-100 z-20 cursor-pointer hover:w-2 hover:h-2 hover:bg-red-600 hover:z-30";
} }
setPointInactive(point) { setPointInactive(point) {
// Inactive state: small light red dots, no border // Inactive state: small light red dots, no border
point.className = 'map-point absolute w-1 h-1 bg-red-200 rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer hover:w-1.5 hover:h-1.5 hover:bg-red-400 hover:z-30'; point.className =
"map-point absolute w-[0.18rem] h-[0.18rem] bg-white opacity-[0.7] rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer hover:w-1.5 hover:h-1.5 hover:bg-red-400 hover:z-30 hover:opacity-[1.0]";
} }
showTooltip(event) { showTooltip(event) {
const point = event.target; const point = event.target;
const tooltipText = point.dataset.tooltipText; const tooltipText = point.dataset.tooltipText;
const placeId = point.dataset.placeId;
if (this.tooltip && tooltipText) { // Don't show NEW tooltip if scrolling blocked (behavior 4)
// Clear any existing timeout if (this.isNewPopupBlocked(placeId)) {
if (this.tooltipTimeout) { return;
clearTimeout(this.tooltipTimeout);
} }
// Set tooltip position immediately but keep it invisible if (this.tooltip && tooltipText) {
// Set tooltip content and position
this.tooltip.textContent = tooltipText; this.tooltip.textContent = tooltipText;
this.updateTooltipPosition(event); this.updateTooltipPosition(event);
// Show tooltip after 0.5 second delay // Show immediately for map points (behavior 1)
this.tooltipTimeout = setTimeout(() => { this.clearTimeouts();
this.tooltip.classList.remove('opacity-0'); this.tooltip.classList.remove("opacity-0");
this.tooltip.classList.add('opacity-100'); this.tooltip.classList.add("opacity-100");
}, 500); this.isTooltipVisible = true;
} }
} }
hideTooltip() { hideTooltip() {
// Clear any pending tooltip timeout // Hide after 150ms delay (behavior 3)
if (this.tooltipTimeout) { // NOTE: This is NOT blocked during scroll - existing popup should close when mouse leaves
clearTimeout(this.tooltipTimeout); this.clearTimeouts();
this.tooltipTimeout = null; this.hideTimeout = setTimeout(() => {
}
if (this.tooltip) { if (this.tooltip) {
this.tooltip.classList.remove('opacity-100'); this.tooltip.classList.remove("opacity-100");
this.tooltip.classList.add('opacity-0'); this.tooltip.classList.add("opacity-0");
this.isTooltipVisible = false;
} }
}, 150);
} }
updateTooltipPosition(event) { updateTooltipPosition(event) {
@@ -647,9 +689,9 @@ export class PlacesMap extends HTMLElement {
if (!placeId) return; if (!placeId) return;
// Emit custom event for place selection // Emit custom event for place selection
const placeSelectedEvent = new CustomEvent('place-map-clicked', { const placeSelectedEvent = new CustomEvent("place-map-clicked", {
detail: { placeId }, detail: { placeId },
bubbles: true bubbles: true,
}); });
this.dispatchEvent(placeSelectedEvent); this.dispatchEvent(placeSelectedEvent);
@@ -658,23 +700,78 @@ export class PlacesMap extends HTMLElement {
if (placeContainer) { if (placeContainer) {
// Smooth scroll to the place container // Smooth scroll to the place container
placeContainer.scrollIntoView({ placeContainer.scrollIntoView({
behavior: 'smooth', behavior: "smooth",
block: 'center', block: "center",
inline: 'nearest' inline: "nearest",
}); });
// Optional: Add a brief highlight effect // Optional: Add a brief highlight effect
placeContainer.style.transition = 'background-color 0.3s ease'; placeContainer.style.transition = "background-color 0.3s ease";
placeContainer.style.backgroundColor = 'rgb(248 250 252)'; placeContainer.style.backgroundColor = "rgb(248 250 252)";
setTimeout(() => { setTimeout(() => {
placeContainer.style.backgroundColor = ''; placeContainer.style.backgroundColor = "";
}, 1000); }, 1000);
} }
} }
setupHeadingHoverListener() { setupHeadingHoverListener() {
// Listen for heading hover events from place accordions // Listen for heading hover events from place accordions
document.addEventListener('place-heading-hover', this.handleHeadingHoverEvent.bind(this)); document.addEventListener("place-heading-hover", this.boundHandleHeadingHoverEvent);
}
clearTimeouts() {
if (this.showTimeout) {
clearTimeout(this.showTimeout);
this.showTimeout = null;
}
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
this.hideTimeout = null;
}
if (this.scrollBlockTimeout) {
clearTimeout(this.scrollBlockTimeout);
this.scrollBlockTimeout = null;
}
}
checkMousePositionAfterScroll() {
// Check if mouse is currently over a place title
if (this.currentHoveredPlaceId) {
// Simulate a heading hover event to show the tooltip
const showEvent = new CustomEvent("place-heading-hover", {
detail: { placeId: this.currentHoveredPlaceId, action: "show" },
bubbles: true,
});
document.dispatchEvent(showEvent);
}
}
checkExistingPopupAfterScroll() {
// Check if mouse is still over the tile that had the popup before scroll
if (this.currentHoveredPlaceId !== this.placeIdBeforeScroll) {
// Mouse is no longer over the original tile - hide the popup
if (this.tooltip && this.isTooltipVisible) {
this.tooltip.classList.remove("opacity-100");
this.tooltip.classList.add("opacity-0");
this.isTooltipVisible = false;
}
}
// If mouse is still over the same tile, keep the popup open
}
isScrollBlocked() {
// Block NEW popups during and 300ms after scrolling (behavior 4)
const isInScrollPeriod = Date.now() - this.lastScrollTime < 300;
return isInScrollPeriod;
}
isNewPopupBlocked(placeId) {
// Block new popups, but allow existing popup to stay
const isInScrollPeriod = Date.now() - this.lastScrollTime < 300;
if (!isInScrollPeriod) return false;
// Allow if this is the same popup that was visible before scroll
return placeId !== this.placeIdBeforeScroll;
} }
handleHeadingHoverEvent(event) { handleHeadingHoverEvent(event) {
@@ -683,66 +780,79 @@ export class PlacesMap extends HTMLElement {
if (!mapPoint) return; if (!mapPoint) return;
if (action === 'show') { if (action === "show") {
// Give the point a subtle highlight by making it larger immediately // Track the currently hovered place ID
mapPoint.classList.remove('w-1', 'h-1', 'w-1.5', 'h-1.5'); this.currentHoveredPlaceId = placeId;
mapPoint.classList.add('w-2', 'h-2');
mapPoint.style.zIndex = '25';
// Show tooltip for the corresponding map point after delay // Give the point a subtle highlight by making it larger immediately
const tooltipText = mapPoint.dataset.tooltipText; mapPoint.classList.remove("w-1", "h-1", "w-1.5", "h-1.5");
if (this.tooltip && tooltipText) { mapPoint.classList.add("w-2", "h-2");
// Clear any existing timeout mapPoint.style.zIndex = "25";
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout); // Don't show NEW tooltip if scrolling blocked (behavior 4)
if (this.isNewPopupBlocked(placeId)) {
return;
} }
// Set tooltip position immediately but keep it invisible const tooltipText = mapPoint.dataset.tooltipText;
if (this.tooltip && tooltipText) {
// Set tooltip content and position
this.tooltip.textContent = tooltipText; this.tooltip.textContent = tooltipText;
// Position tooltip at the map point location // Position tooltip at the map point location
const pointRect = mapPoint.getBoundingClientRect(); const pointRect = mapPoint.getBoundingClientRect();
const mapRect = this.mapElement.getBoundingClientRect(); const mapRect = this.mapElement.getBoundingClientRect();
const x = pointRect.left - mapRect.left + (pointRect.width / 2); const x = pointRect.left - mapRect.left + pointRect.width / 2;
const y = pointRect.top - mapRect.top + (pointRect.height / 2); const y = pointRect.top - mapRect.top + pointRect.height / 2;
this.tooltip.style.left = `${x}px`; this.tooltip.style.left = `${x}px`;
this.tooltip.style.top = `${y}px`; this.tooltip.style.top = `${y}px`;
// Show tooltip after 0.5 second delay // Clear any existing timeouts
this.tooltipTimeout = setTimeout(() => { this.clearTimeouts();
this.tooltip.classList.remove('opacity-0');
this.tooltip.classList.add('opacity-100');
}, 500);
}
} else if (action === 'hide') {
// Clear any pending tooltip timeout
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
}
// Hide tooltip if (this.isTooltipVisible) {
if (this.tooltip) { // Behavior 2b: Popup was visible -- instant popup
this.tooltip.classList.remove('opacity-100'); this.tooltip.classList.remove("opacity-0");
this.tooltip.classList.add('opacity-0'); this.tooltip.classList.add("opacity-100");
} else {
// Behavior 2a: No popup was visible -- Popup after 150ms
this.showTimeout = setTimeout(() => {
this.tooltip.classList.remove("opacity-0");
this.tooltip.classList.add("opacity-100");
this.isTooltipVisible = true;
}, 150);
} }
}
} else if (action === "hide") {
// Clear the currently hovered place ID
this.currentHoveredPlaceId = "";
// Behavior 3: Leave tile or map point -- close after 150ms delay
// NOTE: This is NOT blocked during scroll - existing popup should close when mouse leaves
this.clearTimeouts();
this.hideTimeout = setTimeout(() => {
if (this.tooltip) {
this.tooltip.classList.remove("opacity-100");
this.tooltip.classList.add("opacity-0");
this.isTooltipVisible = false;
}
}, 150);
// Remove point highlight - restore original size based on current state // Remove point highlight - restore original size based on current state
mapPoint.classList.remove('w-2', 'h-2'); mapPoint.classList.remove("w-2", "h-2");
// Check if this point is currently active or inactive // Check if this point is currently active or inactive
if (mapPoint.className.includes('bg-red-500')) { if (mapPoint.className.includes("bg-red-500")) {
// Active point // Active point
mapPoint.classList.add('w-1.5', 'h-1.5'); mapPoint.classList.add("w-1.5", "h-1.5");
} else { } else {
// Inactive point // Inactive point
mapPoint.classList.add('w-1', 'h-1'); mapPoint.classList.add("w-1", "h-1");
} }
mapPoint.style.zIndex = ''; // Reset to default mapPoint.style.zIndex = ""; // Reset to default
} }
} }
disconnectedCallback() { disconnectedCallback() {
// Clean up intersection observer // Clean up intersection observer
if (this.intersectionObserver) { if (this.intersectionObserver) {
@@ -750,18 +860,19 @@ export class PlacesMap extends HTMLElement {
this.intersectionObserver = null; this.intersectionObserver = null;
} }
// Clean up tooltip timeout // Clean up new tooltip timeouts
if (this.tooltipTimeout) { this.clearTimeouts();
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
}
// Clean up heading hover listener // Clean up heading hover listener
document.removeEventListener('place-heading-hover', this.handleHeadingHoverEvent.bind(this)); document.removeEventListener("place-heading-hover", this.boundHandleHeadingHoverEvent);
// Clean up scroll listeners
window.removeEventListener("scroll", this.boundHandleScroll);
document.removeEventListener("scroll", this.boundHandleScroll);
} }
} }
// Register the custom elements // Register the custom elements
customElements.define('places-filter', PlacesFilter); customElements.define("places-filter", PlacesFilter);
customElements.define('place-accordion', PlaceAccordion); customElements.define("place-accordion", PlaceAccordion);
customElements.define('places-map', PlacesMap); customElements.define("places-map", PlacesMap);