From e8855a6c3c570423fe2ab30de8c5f1f8df4a0e40 Mon Sep 17 00:00:00 2001 From: Simon Martens Date: Sun, 28 Sep 2025 11:59:58 +0200 Subject: [PATCH] map --- app/kgpz.go | 4 +- controllers/ort_controller.go | 9 +- viewmodels/place_view.go | 78 ++- views/assets/Europe_laea_location_map.svg | 257 +++++++++ views/assets/scripts.js | 429 ++++++++++----- views/assets/style.css | 2 +- views/public/Europe_laea_location_map.svg | 257 +++++++++ .../components/_unified_piece_entry.gohtml | 2 +- .../ort/components/_place_pieces.gohtml | 17 +- views/routes/ort/fragment/body.gohtml | 2 +- views/routes/ort/overview/body.gohtml | 81 +-- views/transform/generic-filter.js | 38 +- views/transform/places.js | 500 +++++++++++++++++- 13 files changed, 1478 insertions(+), 198 deletions(-) create mode 100644 views/assets/Europe_laea_location_map.svg create mode 100644 views/public/Europe_laea_location_map.svg diff --git a/app/kgpz.go b/app/kgpz.go index 72917e4..8d3f690 100644 --- a/app/kgpz.go +++ b/app/kgpz.go @@ -161,8 +161,8 @@ func (k *KGPZ) Routes(srv *fiber.App) error { srv.Get(SEARCH_URL, controllers.GetSearch(k.Library, k.Search)) srv.Get(FILTER_URL, controllers.GetQuickFilter(k.Library)) - srv.Get("/ort/fragment/:place", controllers.GetPlaceFragment(k.Library)) - srv.Get(PLACE_OVERVIEW_URL, controllers.GetPlace(k.Library)) + srv.Get("/ort/fragment/:place", controllers.GetPlaceFragment(k.Library, k.Geonames)) + srv.Get(PLACE_OVERVIEW_URL, controllers.GetPlace(k.Library, k.Geonames)) srv.Get(CATEGORY_OVERVIEW_URL, controllers.GetCategory(k.Library)) srv.Get(AGENTS_OVERVIEW_URL, controllers.GetAgents(k.Library)) srv.Get(PIECE_PAGE_URL, controllers.GetPieceWithPage(k.Library)) diff --git a/controllers/ort_controller.go b/controllers/ort_controller.go index 2d6dee1..1cb2ef1 100644 --- a/controllers/ort_controller.go +++ b/controllers/ort_controller.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging" + "github.com/Theodor-Springmann-Stiftung/kgpz_web/providers/geonames" "github.com/Theodor-Springmann-Stiftung/kgpz_web/viewmodels" "github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels" "github.com/gofiber/fiber/v2" @@ -13,13 +14,13 @@ const ( DEFAULT_PLACE = "" ) -func GetPlace(kgpz *xmlmodels.Library) fiber.Handler { +func GetPlace(kgpz *xmlmodels.Library, geonamesProvider *geonames.GeonamesProvider) fiber.Handler { return func(c *fiber.Ctx) error { placeID := c.Params("place", DEFAULT_PLACE) placeID = strings.ToLower(placeID) // Get places data using view model - places := viewmodels.PlacesView(placeID, kgpz) + places := viewmodels.PlacesView(placeID, kgpz, geonamesProvider) // If no places found at all, return 404 if len(places.Places) == 0 { @@ -48,13 +49,13 @@ func GetPlace(kgpz *xmlmodels.Library) fiber.Handler { } } -func GetPlaceFragment(kgpz *xmlmodels.Library) fiber.Handler { +func GetPlaceFragment(kgpz *xmlmodels.Library, geonamesProvider *geonames.GeonamesProvider) fiber.Handler { return func(c *fiber.Ctx) error { placeID := c.Params("place", DEFAULT_PLACE) placeID = strings.ToLower(placeID) // Get places data using view model - places := viewmodels.PlacesView(placeID, kgpz) + places := viewmodels.PlacesView(placeID, kgpz, geonamesProvider) // If no places found at all, return 404 if len(places.Places) == 0 { diff --git a/viewmodels/place_view.go b/viewmodels/place_view.go index dfbb8e6..3d7bbb0 100644 --- a/viewmodels/place_view.go +++ b/viewmodels/place_view.go @@ -1,11 +1,13 @@ package viewmodels import ( + "encoding/json" "maps" "slices" "strings" "github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels" + "github.com/Theodor-Springmann-Stiftung/kgpz_web/providers/geonames" ) // PlacesListView represents the data for the places overview @@ -17,6 +19,16 @@ type PlacesListView struct { Sorted []string SelectedPlace *PlaceDetailView TotalPiecesWithPlaces int + PlacesJSON string +} + +// MapPlace represents a place for the map component +type MapPlace struct { + ID string `json:"id"` + Name string `json:"name"` + ToponymName string `json:"toponymName"` + Lat string `json:"lat"` + Lng string `json:"lng"` } // PlaceDetailView represents a specific place with its associated pieces @@ -26,7 +38,7 @@ type PlaceDetailView struct { } // PlacesView returns places data for the overview page -func PlacesView(placeID string, lib *xmlmodels.Library) *PlacesListView { +func PlacesView(placeID string, lib *xmlmodels.Library, geonamesProvider *geonames.GeonamesProvider) *PlacesListView { res := PlacesListView{ Search: placeID, Places: make(map[string]xmlmodels.Place), @@ -76,6 +88,9 @@ func PlacesView(placeID string, lib *xmlmodels.Library) *PlacesListView { slices.Sort(res.Sorted) res.TotalPiecesWithPlaces = totalPiecesWithPlaces + // Generate JSON data for map + res.PlacesJSON = generatePlacesJSON(res.Places, geonamesProvider) + return &res } @@ -111,4 +126,65 @@ func GetPlaceDetail(place xmlmodels.Place, lib *xmlmodels.Library) *PlaceDetailV }) return detail +} + +// generatePlacesJSON creates JSON data for the map component +func generatePlacesJSON(places map[string]xmlmodels.Place, geonamesProvider *geonames.GeonamesProvider) string { + if geonamesProvider == nil { + return "[]" + } + + mapPlaces := make([]MapPlace, 0) + + for _, place := range places { + if place.Geo == "" { + continue + } + + // Get geonames data + geoPlace := geonamesProvider.Place(place.Geo) + if geoPlace == nil || geoPlace.Lat == "" || geoPlace.Lng == "" { + continue + } + + // Get main place name + mainName := place.ID + if len(place.Names) > 0 { + mainName = place.Names[0] + } + + // Get modern place name (toponym) + toponymName := "" + for _, altName := range geoPlace.AlternateNames { + if altName.Lang == "de" { + toponymName = altName.Name + break + } + } + if toponymName == "" { + toponymName = geoPlace.Name + } + + mapPlace := MapPlace{ + ID: place.ID, + Name: mainName, + ToponymName: toponymName, + Lat: geoPlace.Lat, + Lng: geoPlace.Lng, + } + + mapPlaces = append(mapPlaces, mapPlace) + } + + // Sort by name for consistent output + slices.SortFunc(mapPlaces, func(a, b MapPlace) int { + return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) + }) + + jsonData, err := json.Marshal(mapPlaces) + if err != nil { + return "[]" + } + + return string(jsonData) } \ No newline at end of file diff --git a/views/assets/Europe_laea_location_map.svg b/views/assets/Europe_laea_location_map.svg new file mode 100644 index 0000000..75fa8f4 --- /dev/null +++ b/views/assets/Europe_laea_location_map.svg @@ -0,0 +1,257 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/views/assets/scripts.js b/views/assets/scripts.js index ba43a75..8453474 100644 --- a/views/assets/scripts.js +++ b/views/assets/scripts.js @@ -2,7 +2,7 @@ document.body.addEventListener("htmx:configRequest", function(a) { let e = a.detail.elt; e.id === "search" && e.value === "" && (a.detail.parameters = {}, a.detail.path = window.location.pathname + window.location.search); }); -class A extends HTMLElement { +class N extends HTMLElement { constructor() { super(); } @@ -35,8 +35,8 @@ class A extends HTMLElement { }); } } -customElements.define("person-jump-filter", A); -class H extends HTMLElement { +customElements.define("person-jump-filter", N); +class $ extends HTMLElement { connectedCallback() { const e = this.querySelector("#place-search"); e && e.addEventListener("input", (t) => { @@ -49,8 +49,8 @@ class H extends HTMLElement { }); } } -customElements.define("place-jump-filter", H); -class B extends HTMLElement { +customElements.define("place-jump-filter", $); +class O extends HTMLElement { connectedCallback() { const e = this.querySelector("#category-search"); e && e.addEventListener("input", (t) => { @@ -63,8 +63,8 @@ class B extends HTMLElement { }); } } -customElements.define("category-jump-filter", B); -class M extends HTMLElement { +customElements.define("category-jump-filter", O); +class R extends HTMLElement { constructor() { super(), this.issuesByYear = {}; } @@ -179,8 +179,8 @@ class M extends HTMLElement { i.disabled = !o; } } -customElements.define("year-jump-filter", M); -class N extends HTMLElement { +customElements.define("year-jump-filter", R); +class V extends HTMLElement { constructor() { super(), this.isOpen = !1; } @@ -242,8 +242,8 @@ class N extends HTMLElement { this.isOpen && t && i && !t.contains(e.target) && !this.contains(e.target) && this.hideFilter(); } } -customElements.define("schnellauswahl-button", N); -class $ extends HTMLElement { +customElements.define("schnellauswahl-button", V); +class z extends HTMLElement { constructor() { super(), this.isOpen = !1; } @@ -319,7 +319,7 @@ class $ extends HTMLElement { this.isOpen && !this.contains(e.target) && this.hideMenu(); } } -customElements.define("navigation-menu", $); +customElements.define("navigation-menu", z); document.addEventListener("DOMContentLoaded", function() { document.addEventListener("click", function(a) { 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 w = []; +const T = []; document.addEventListener("DOMContentLoaded", () => { - S(); + I(); }); -const S = function() { - for (; w.length > 0; ) { - const a = w.shift(); +const I = function() { + for (; T.length > 0; ) { + const a = T.shift(); try { a(); } catch (e) { @@ -351,7 +351,7 @@ const S = function() { } } }; -class O extends HTMLElement { +class D extends HTMLElement { constructor() { super(), this.scrollTimeout = null, this.clickHandlers = [], this.manualNavigation = !1, this.handleScroll = this.handleScroll.bind(this); } @@ -492,9 +492,9 @@ class O extends HTMLElement { document.documentElement.offsetHeight ), s = window.innerHeight, o = n - s, r = o > 0 ? window.scrollY / o : 0, l = t.clientHeight, d = t.scrollHeight - l; if (d > 0) { - const u = r * d, h = i.getBoundingClientRect(), p = t.getBoundingClientRect(), f = h.top - p.top + t.scrollTop, m = l / 2, T = f - m, y = 0.7, I = y * u + (1 - y) * T, v = Math.max(0, Math.min(d, I)), q = t.scrollTop; - Math.abs(v - q) > 10 && t.scrollTo({ - top: v, + 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; + Math.abs(x - E) > 10 && t.scrollTo({ + top: x, behavior: "smooth" }); } @@ -513,8 +513,8 @@ class O extends HTMLElement { e && (e.style.opacity = "0", e.style.height = "0"), this.sections = null, this.navLinks = null, this.clickHandlers = [], this.manualNavigation = !1; } } -customElements.define("akteure-scrollspy", O); -class V extends HTMLElement { +customElements.define("akteure-scrollspy", D); +class F extends HTMLElement { constructor() { super(), this.searchInput = null, this.placeCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0; } @@ -566,15 +566,20 @@ class V 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`); } } -class R extends HTMLElement { +class j extends HTMLElement { constructor() { super(), this.isExpanded = !1, this.isLoading = !1, this.hasLoaded = !1; } connectedCallback() { - this.setupAccordion(), this.setupEventListeners(); + this.setupAccordion(), this.setupEventListeners(), this.updateBorders(), this.setupMapEventListeners(), this.setupHoverEvents(); } disconnectedCallback() { - this.cleanupEventListeners(); + this.cleanupEventListeners(), this.cleanupMapEventListeners(); + } + cleanupMapEventListeners() { + document.removeEventListener("place-map-clicked", this.handleMapClick.bind(this)); + const e = this.querySelector(".cursor-pointer"); + e && (e.removeEventListener("mouseenter", this.handleHeadingHover.bind(this)), e.removeEventListener("mouseleave", this.handleHeadingLeave.bind(this))); } setupAccordion() { if (!this.querySelector(".accordion-chevron")) { @@ -594,6 +599,39 @@ class R extends HTMLElement { cleanupEventListeners() { this.removeEventListener("click", this.handleClick.bind(this)); } + setupMapEventListeners() { + document.addEventListener("place-map-clicked", this.handleMapClick.bind(this)); + } + setupHoverEvents() { + const e = this.querySelector(".cursor-pointer"); + e && (e.addEventListener("mouseenter", this.handleHeadingHover.bind(this)), e.addEventListener("mouseleave", this.handleHeadingLeave.bind(this))); + } + handleHeadingHover() { + const e = this.getAttribute("data-place-id"); + if (e) { + const t = new CustomEvent("place-heading-hover", { + detail: { placeId: e, action: "show" }, + bubbles: !0 + }); + document.dispatchEvent(t); + } + } + handleHeadingLeave() { + const e = this.getAttribute("data-place-id"); + if (e) { + const t = new CustomEvent("place-heading-hover", { + detail: { placeId: e, action: "hide" }, + bubbles: !0 + }); + document.dispatchEvent(t); + } + } + handleMapClick(e) { + const t = e.detail.placeId, i = this.getAttribute("data-place-id"); + t === i && !this.isExpanded && setTimeout(() => { + this.expand(); + }, 800); + } handleClick(e) { const t = this.querySelector("[data-content]"); t && t.contains(e.target) || this.toggle(); @@ -603,12 +641,12 @@ class R extends HTMLElement { } expand() { if (this.isLoading) return; - this.isExpanded = !0, this.updateChevron(); + this.isExpanded = !0, this.updateChevron(), this.updateBorders(); const e = this.querySelector("[data-content]"); e && (this.hasLoaded ? e.style.maxHeight = e.scrollHeight + "px" : this.loadContent()); } collapse() { - this.isExpanded = !1, this.updateChevron(); + this.isExpanded = !1, this.updateChevron(), this.updateBorders(); const e = this.querySelector("[data-content]"); e && (e.style.maxHeight = "0px"); } @@ -629,10 +667,165 @@ class R extends HTMLElement { const e = this.querySelector(".accordion-chevron"); e && (this.isExpanded ? e.style.transform = "rotate(180deg)" : e.style.transform = "rotate(0deg)"); } + updateBorders() { + 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"); + } } -customElements.define("places-filter", V); -customElements.define("place-accordion", R); -class z extends HTMLElement { +class K extends HTMLElement { + constructor() { + super(), this.places = [], this.mapElement = null, this.pointsContainer = null, this.intersectionObserver = null, this.mapPoints = /* @__PURE__ */ new Map(), this.tooltip = null, this.tooltipTimeout = null; + } + connectedCallback() { + this.parseData(), this.render(), this.initializeMap(), setTimeout(() => { + this.initializeScrollspy(); + }, 200), this.setupHeadingHoverListener(); + } + parseData() { + try { + const e = this.dataset.places; + e && (this.places = JSON.parse(e)); + } catch (e) { + console.error("Failed to parse places data:", e), this.places = []; + } + } + render() { + this.innerHTML = ` +
+
+ Map of Europe +
+
+ +
+
+ `, this.mapElement = this.querySelector(".map-container"), this.pointsContainer = this.querySelector(".points-container"), this.tooltip = this.querySelector(".map-tooltip"); + } + initializeMap() { + if (!this.places.length || !this.pointsContainer) + return; + 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; + return { x: L, y: C }; + }, n = []; + this.places.forEach((s) => { + if (s.lat && s.lng) { + const o = parseFloat(s.lat), r = parseFloat(s.lng), l = i(o, r); + if (l.x >= 0 && l.x <= 100 && l.y >= 0 && l.y <= 100) { + n.push(l); + const c = document.createElement("div"); + c.className = "map-point absolute w-1 h-1 bg-red-200 border border-red-300 rounded-full shadow-sm -translate-x-1/2 -translate-y-1/2 transition-all duration-300 z-10 cursor-pointer", 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})` : ""}`; + 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); + } + autoZoomToPoints(e) { + let t = 100, i = 0, n = 100, s = 0; + e.forEach((b) => { + b.x < t && (t = b.x), b.x > i && (i = b.x), b.y < n && (n = b.y), b.y > s && (s = b.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; + let v = { x: d, y: h, width: g, height: m }; + if (w > y) { + const b = g / y; + v.y = h - (b - m) / 2, v.height = b; + } else { + const b = m * y; + v.x = d - (b - g) / 2, v.width = b; + } + const x = 100 / v.width, E = -v.x, L = -v.y, C = `scale(${x}) translate(${E}%, ${L}%)`, P = this.querySelector(".transform-wrapper"); + P && (P.style.transform = C); + } + initializeScrollspy() { + const e = document.querySelectorAll("place-accordion[data-place-id]"); + e.length && (this.mapPoints.forEach((t) => { + this.setPointInactive(t); + }), this.intersectionObserver = new IntersectionObserver( + (t) => { + t.forEach((i) => { + const n = i.target.getAttribute("data-place-id"), s = this.mapPoints.get(n); + s && (i.isIntersecting ? this.setPointActive(s) : this.setPointInactive(s)); + }); + }, + { + // Trigger when element enters viewport + threshold: 0.1, + // No root margin for precise detection + rootMargin: "0px" + } + ), e.forEach((t) => { + this.intersectionObserver.observe(t); + }), setTimeout(() => { + e.forEach((t) => { + const i = t.getBoundingClientRect(), n = i.top < window.innerHeight && i.bottom > 0, s = t.getAttribute("data-place-id"), o = this.mapPoints.get(s); + o && n && this.setPointActive(o); + }); + }, 50)); + } + setPointActive(e) { + 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:z-30"; + } + setPointInactive(e) { + e.className = "map-point absolute w-1 h-1 bg-red-200 border border-red-300 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:z-30"; + } + showTooltip(e) { + const i = e.target.dataset.tooltipText; + this.tooltip && i && (this.tooltipTimeout && clearTimeout(this.tooltipTimeout), this.tooltip.textContent = i, this.updateTooltipPosition(e), this.tooltipTimeout = setTimeout(() => { + this.tooltip.classList.remove("opacity-0"), this.tooltip.classList.add("opacity-100"); + }, 1e3)); + } + hideTooltip() { + this.tooltipTimeout && (clearTimeout(this.tooltipTimeout), this.tooltipTimeout = null), this.tooltip && (this.tooltip.classList.remove("opacity-100"), this.tooltip.classList.add("opacity-0")); + } + updateTooltipPosition(e) { + if (!this.tooltip) return; + const t = this.mapElement.getBoundingClientRect(), i = e.clientX - t.left, n = e.clientY - t.top; + this.tooltip.style.left = `${i}px`, this.tooltip.style.top = `${n}px`; + } + scrollToPlace(e) { + const t = e.target.dataset.placeId; + if (!t) return; + const i = new CustomEvent("place-map-clicked", { + detail: { placeId: t }, + bubbles: !0 + }); + this.dispatchEvent(i); + const n = document.querySelector(`place-accordion[data-place-id="${t}"]`); + n && (n.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest" + }), n.style.transition = "background-color 0.3s ease", n.style.backgroundColor = "rgb(248 250 252)", setTimeout(() => { + n.style.backgroundColor = ""; + }, 1e3)); + } + setupHeadingHoverListener() { + document.addEventListener("place-heading-hover", this.handleHeadingHoverEvent.bind(this)); + } + handleHeadingHoverEvent(e) { + const { placeId: t, action: i } = e.detail, n = this.mapPoints.get(t); + if (n) + 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"; + const s = n.dataset.tooltipText; + if (this.tooltip && s) { + this.tooltipTimeout && clearTimeout(this.tooltipTimeout), 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; + this.tooltip.style.left = `${l}px`, this.tooltip.style.top = `${c}px`, this.tooltipTimeout = setTimeout(() => { + this.tooltip.classList.remove("opacity-0"), this.tooltip.classList.add("opacity-100"); + }, 1e3); + } + } 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 = ""); + } + 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)); + } +} +customElements.define("places-filter", F); +customElements.define("place-accordion", j); +customElements.define("places-map", K); +class W extends HTMLElement { constructor() { super(), this.searchInput = null, this.itemCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0; } @@ -644,15 +837,13 @@ class z extends HTMLElement { } render() { this.innerHTML = ` -
- -
+ `; } setupEventListeners() { @@ -669,7 +860,7 @@ class z extends HTMLElement { itemsFound: this.itemCards.length, countElement: this.countElement, searchAttributes: this.searchAttributes - }), this.countElement && (this.originalCount = this.itemCards.length); + }), this.countElement && (this.originalCount = this.itemCards.length, this.countElement.style.display = "none"); } handleSearchInput(e) { this.debounceTimer && clearTimeout(this.debounceTimer), this.debounceTimer = setTimeout(() => { @@ -694,19 +885,11 @@ class z extends HTMLElement { }), this.updateCountDisplay(i, e); } updateCountDisplay(e, t) { - if (this.countElement) - if (t === "") - this.countElement.textContent = `Alle ${this.itemType} (${this.originalCount})`; - else if (e === 0) - this.countElement.textContent = `Keine ${this.itemType} gefunden für "${t}"`; - else { - const i = e === 1 ? this.itemTypeSingular : this.itemType; - this.countElement.textContent = `${e} von ${this.originalCount} ${i}`; - } + 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", z); -class D extends HTMLElement { +customElements.define("generic-filter", W); +class Y extends HTMLElement { constructor() { super(), this.resizeObserver = null; } @@ -839,13 +1022,13 @@ class D extends HTMLElement { if (l) h = l; else { - const f = this.getIssueContext(i); - h = f ? `${f}, ${i}` : `${i}`; + const g = this.getIssueContext(i); + h = g ? `${g}, ${i}` : `${i}`; } if (d.innerHTML = h, s && i === s) { d.style.position = "relative"; - const f = d.querySelector(".target-page-dot"); - f && f.remove(); + const g = d.querySelector(".target-page-dot"); + g && g.remove(); const m = 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); } @@ -1059,7 +1242,7 @@ class D extends HTMLElement { return "KGPZ"; } } -customElements.define("single-page-viewer", D); +customElements.define("single-page-viewer", Y); document.body.addEventListener("htmx:beforeRequest", function(a) { const e = document.querySelector("single-page-viewer"); e && e.style.display !== "none" && (console.log("Cleaning up single page viewer before HTMX navigation"), e.close()); @@ -1068,7 +1251,7 @@ window.addEventListener("beforeunload", function() { const a = document.querySelector("single-page-viewer"); a && a.close(); }); -class j extends HTMLElement { +class Z extends HTMLElement { constructor() { super(), this.isVisible = !1, this.scrollHandler = null, this.htmxAfterSwapHandler = null; } @@ -1109,8 +1292,8 @@ class j extends HTMLElement { }); } } -customElements.define("scroll-to-top-button", j); -class F extends HTMLElement { +customElements.define("scroll-to-top-button", Z); +class J extends HTMLElement { constructor() { super(), this.pageObserver = null, this.pageContainers = /* @__PURE__ */ new Map(), this.singlePageViewerActive = !1, this.singlePageViewerCurrentPage = null, this.boundHandleSinglePageViewer = this.handleSinglePageViewer.bind(this); } @@ -1229,8 +1412,8 @@ class F 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(); } } -customElements.define("inhaltsverzeichnis-scrollspy", F); -class K extends HTMLElement { +customElements.define("inhaltsverzeichnis-scrollspy", J); +class X extends HTMLElement { constructor() { super(), this.innerHTML = `