This commit is contained in:
Simon Martens
2025-09-28 11:59:58 +02:00
parent adc45f2212
commit e8855a6c3c
13 changed files with 1478 additions and 198 deletions

View File

@@ -161,8 +161,8 @@ func (k *KGPZ) Routes(srv *fiber.App) error {
srv.Get(SEARCH_URL, controllers.GetSearch(k.Library, k.Search)) srv.Get(SEARCH_URL, controllers.GetSearch(k.Library, k.Search))
srv.Get(FILTER_URL, controllers.GetQuickFilter(k.Library)) srv.Get(FILTER_URL, controllers.GetQuickFilter(k.Library))
srv.Get("/ort/fragment/:place", controllers.GetPlaceFragment(k.Library)) srv.Get("/ort/fragment/:place", controllers.GetPlaceFragment(k.Library, k.Geonames))
srv.Get(PLACE_OVERVIEW_URL, controllers.GetPlace(k.Library)) srv.Get(PLACE_OVERVIEW_URL, controllers.GetPlace(k.Library, k.Geonames))
srv.Get(CATEGORY_OVERVIEW_URL, controllers.GetCategory(k.Library)) srv.Get(CATEGORY_OVERVIEW_URL, controllers.GetCategory(k.Library))
srv.Get(AGENTS_OVERVIEW_URL, controllers.GetAgents(k.Library)) srv.Get(AGENTS_OVERVIEW_URL, controllers.GetAgents(k.Library))
srv.Get(PIECE_PAGE_URL, controllers.GetPieceWithPage(k.Library)) srv.Get(PIECE_PAGE_URL, controllers.GetPieceWithPage(k.Library))

View File

@@ -4,6 +4,7 @@ import (
"strings" "strings"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging" "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/viewmodels"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels" "github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -13,13 +14,13 @@ const (
DEFAULT_PLACE = "" 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 { return func(c *fiber.Ctx) error {
placeID := c.Params("place", DEFAULT_PLACE) placeID := c.Params("place", DEFAULT_PLACE)
placeID = strings.ToLower(placeID) placeID = strings.ToLower(placeID)
// Get places data using view model // 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 no places found at all, return 404
if len(places.Places) == 0 { 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 { return func(c *fiber.Ctx) error {
placeID := c.Params("place", DEFAULT_PLACE) placeID := c.Params("place", DEFAULT_PLACE)
placeID = strings.ToLower(placeID) placeID = strings.ToLower(placeID)
// Get places data using view model // 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 no places found at all, return 404
if len(places.Places) == 0 { if len(places.Places) == 0 {

View File

@@ -1,11 +1,13 @@
package viewmodels package viewmodels
import ( import (
"encoding/json"
"maps" "maps"
"slices" "slices"
"strings" "strings"
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels" "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 // PlacesListView represents the data for the places overview
@@ -17,6 +19,16 @@ type PlacesListView struct {
Sorted []string Sorted []string
SelectedPlace *PlaceDetailView SelectedPlace *PlaceDetailView
TotalPiecesWithPlaces int 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 // PlaceDetailView represents a specific place with its associated pieces
@@ -26,7 +38,7 @@ type PlaceDetailView struct {
} }
// PlacesView returns places data for the overview page // 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{ res := PlacesListView{
Search: placeID, Search: placeID,
Places: make(map[string]xmlmodels.Place), Places: make(map[string]xmlmodels.Place),
@@ -76,6 +88,9 @@ func PlacesView(placeID string, lib *xmlmodels.Library) *PlacesListView {
slices.Sort(res.Sorted) slices.Sort(res.Sorted)
res.TotalPiecesWithPlaces = totalPiecesWithPlaces res.TotalPiecesWithPlaces = totalPiecesWithPlaces
// Generate JSON data for map
res.PlacesJSON = generatePlacesJSON(res.Places, geonamesProvider)
return &res return &res
} }
@@ -112,3 +127,64 @@ func GetPlaceDetail(place xmlmodels.Place, lib *xmlmodels.Library) *PlaceDetailV
return detail 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)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.2 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 A extends HTMLElement { class N extends HTMLElement {
constructor() { constructor() {
super(); super();
} }
@@ -35,8 +35,8 @@ class A extends HTMLElement {
}); });
} }
} }
customElements.define("person-jump-filter", A); customElements.define("person-jump-filter", N);
class H extends HTMLElement { class $ 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 H extends HTMLElement {
}); });
} }
} }
customElements.define("place-jump-filter", H); customElements.define("place-jump-filter", $);
class B extends HTMLElement { class O 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,8 +63,8 @@ class B extends HTMLElement {
}); });
} }
} }
customElements.define("category-jump-filter", B); customElements.define("category-jump-filter", O);
class M extends HTMLElement { class R extends HTMLElement {
constructor() { constructor() {
super(), this.issuesByYear = {}; super(), this.issuesByYear = {};
} }
@@ -179,8 +179,8 @@ class M extends HTMLElement {
i.disabled = !o; i.disabled = !o;
} }
} }
customElements.define("year-jump-filter", M); customElements.define("year-jump-filter", R);
class N extends HTMLElement { class V extends HTMLElement {
constructor() { constructor() {
super(), this.isOpen = !1; 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(); this.isOpen && t && i && !t.contains(e.target) && !this.contains(e.target) && this.hideFilter();
} }
} }
customElements.define("schnellauswahl-button", N); customElements.define("schnellauswahl-button", V);
class $ extends HTMLElement { class z extends HTMLElement {
constructor() { constructor() {
super(), this.isOpen = !1; super(), this.isOpen = !1;
} }
@@ -319,7 +319,7 @@ class $ extends HTMLElement {
this.isOpen && !this.contains(e.target) && this.hideMenu(); this.isOpen && !this.contains(e.target) && this.hideMenu();
} }
} }
customElements.define("navigation-menu", $); customElements.define("navigation-menu", z);
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 w = []; const T = [];
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
S(); I();
}); });
const S = function() { const I = function() {
for (; w.length > 0; ) { for (; T.length > 0; ) {
const a = w.shift(); const a = T.shift();
try { try {
a(); a();
} catch (e) { } catch (e) {
@@ -351,7 +351,7 @@ const S = function() {
} }
} }
}; };
class O extends HTMLElement { class D extends HTMLElement {
constructor() { constructor() {
super(), this.scrollTimeout = null, this.clickHandlers = [], this.manualNavigation = !1, this.handleScroll = this.handleScroll.bind(this); 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 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(), 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; 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(v - q) > 10 && t.scrollTo({ Math.abs(x - E) > 10 && t.scrollTo({
top: v, top: x,
behavior: "smooth" 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; 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); customElements.define("akteure-scrollspy", D);
class V extends HTMLElement { class F 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,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`); 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() { constructor() {
super(), this.isExpanded = !1, this.isLoading = !1, this.hasLoaded = !1; super(), this.isExpanded = !1, this.isLoading = !1, this.hasLoaded = !1;
} }
connectedCallback() { connectedCallback() {
this.setupAccordion(), this.setupEventListeners(); this.setupAccordion(), this.setupEventListeners(), this.updateBorders(), this.setupMapEventListeners(), this.setupHoverEvents();
} }
disconnectedCallback() { 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() { setupAccordion() {
if (!this.querySelector(".accordion-chevron")) { if (!this.querySelector(".accordion-chevron")) {
@@ -594,6 +599,39 @@ class R extends HTMLElement {
cleanupEventListeners() { cleanupEventListeners() {
this.removeEventListener("click", this.handleClick.bind(this)); 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) { handleClick(e) {
const t = this.querySelector("[data-content]"); const t = this.querySelector("[data-content]");
t && t.contains(e.target) || this.toggle(); t && t.contains(e.target) || this.toggle();
@@ -603,12 +641,12 @@ class R extends HTMLElement {
} }
expand() { expand() {
if (this.isLoading) return; if (this.isLoading) return;
this.isExpanded = !0, this.updateChevron(); this.isExpanded = !0, this.updateChevron(), this.updateBorders();
const e = this.querySelector("[data-content]"); const e = this.querySelector("[data-content]");
e && (this.hasLoaded ? e.style.maxHeight = e.scrollHeight + "px" : this.loadContent()); e && (this.hasLoaded ? e.style.maxHeight = e.scrollHeight + "px" : this.loadContent());
} }
collapse() { collapse() {
this.isExpanded = !1, this.updateChevron(); this.isExpanded = !1, this.updateChevron(), this.updateBorders();
const e = this.querySelector("[data-content]"); const e = this.querySelector("[data-content]");
e && (e.style.maxHeight = "0px"); e && (e.style.maxHeight = "0px");
} }
@@ -629,10 +667,165 @@ class R extends HTMLElement {
const e = this.querySelector(".accordion-chevron"); const e = this.querySelector(".accordion-chevron");
e && (this.isExpanded ? e.style.transform = "rotate(180deg)" : e.style.transform = "rotate(0deg)"); 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); class K extends HTMLElement {
customElements.define("place-accordion", R); constructor() {
class z extends HTMLElement { 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 = `
<div class="map-container relative w-full aspect-[5/7] overflow-hidden rounded border border-slate-200 bg-slate-100">
<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">
<div class="points-container absolute top-0 left-0 w-full h-full"></div>
</div>
<!-- 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>
`, 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() { 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;
} }
@@ -644,7 +837,6 @@ class z extends HTMLElement {
} }
render() { render() {
this.innerHTML = ` this.innerHTML = `
<div class="mb-6">
<input <input
type="text" type="text"
id="generic-search" id="generic-search"
@@ -652,7 +844,6 @@ class z extends HTMLElement {
autocomplete="off" autocomplete="off"
class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
> >
</div>
`; `;
} }
setupEventListeners() { setupEventListeners() {
@@ -669,7 +860,7 @@ class z extends HTMLElement {
itemsFound: this.itemCards.length, itemsFound: this.itemCards.length,
countElement: this.countElement, countElement: this.countElement,
searchAttributes: this.searchAttributes searchAttributes: this.searchAttributes
}), this.countElement && (this.originalCount = this.itemCards.length); }), this.countElement && (this.originalCount = this.itemCards.length, this.countElement.style.display = "none");
} }
handleSearchInput(e) { handleSearchInput(e) {
this.debounceTimer && clearTimeout(this.debounceTimer), this.debounceTimer = setTimeout(() => { this.debounceTimer && clearTimeout(this.debounceTimer), this.debounceTimer = setTimeout(() => {
@@ -694,19 +885,11 @@ class z extends HTMLElement {
}), this.updateCountDisplay(i, e); }), this.updateCountDisplay(i, e);
} }
updateCountDisplay(e, t) { updateCountDisplay(e, t) {
if (this.countElement) this.countElement && (t === "" ? this.countElement.style.display = "none" : (this.countElement.style.display = "", e === 0 ? this.countElement.textContent = "(0)" : this.countElement.textContent = `(${e})`));
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}`;
}
} }
} }
customElements.define("generic-filter", z); customElements.define("generic-filter", W);
class D extends HTMLElement { class Y extends HTMLElement {
constructor() { constructor() {
super(), this.resizeObserver = null; super(), this.resizeObserver = null;
} }
@@ -839,13 +1022,13 @@ class D extends HTMLElement {
if (l) if (l)
h = l; h = l;
else { else {
const f = this.getIssueContext(i); const g = this.getIssueContext(i);
h = f ? `${f}, ${i}` : `${i}`; h = g ? `${g}, ${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 f = d.querySelector(".target-page-dot"); const g = d.querySelector(".target-page-dot");
f && f.remove(); g && g.remove();
const m = document.createElement("span"); 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); 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"; return "KGPZ";
} }
} }
customElements.define("single-page-viewer", D); customElements.define("single-page-viewer", Y);
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());
@@ -1068,7 +1251,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 j extends HTMLElement { class Z extends HTMLElement {
constructor() { constructor() {
super(), this.isVisible = !1, this.scrollHandler = null, this.htmxAfterSwapHandler = null; 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); customElements.define("scroll-to-top-button", Z);
class F extends HTMLElement { class J 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);
} }
@@ -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(); 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); customElements.define("inhaltsverzeichnis-scrollspy", J);
class K extends HTMLElement { class X 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">
@@ -1278,11 +1461,11 @@ class K 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", K); customElements.define("error-modal", X);
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 W(a, e, t, i = null) { function _(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");
@@ -1297,11 +1480,11 @@ function W(a, e, t, i = null) {
} }
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 E() { function q() {
document.getElementById("pageModal").classList.add("hidden"); document.getElementById("pageModal").classList.add("hidden");
} }
function Z() { function G() {
if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container")), window.currentActiveIndex = 0, b(), 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(
(t) => { (t) => {
@@ -1310,7 +1493,7 @@ function Z() {
n !== -1 && (i.isIntersecting ? e.add(n) : e.delete(n)); n !== -1 && (i.isIntersecting ? e.add(n) : e.delete(n));
}), e.size > 0) { }), e.size > 0) {
const n = Array.from(e).sort((s, o) => s - o)[0]; const n = Array.from(e).sort((s, o) => s - o)[0];
n !== window.currentActiveIndex && (window.currentActiveIndex = n, b()); n !== window.currentActiveIndex && (window.currentActiveIndex = n, S());
} }
}, },
{ {
@@ -1321,7 +1504,7 @@ function Z() {
}); });
} }
} }
function J() { function U() {
if (window.currentActiveIndex > 0) { if (window.currentActiveIndex > 0) {
let a = -1; let a = -1;
const e = []; const e = [];
@@ -1338,11 +1521,11 @@ function J() {
a === -1 && t > 0 && (a = t - 1), a >= 0 && (window.currentActiveIndex = a, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({ a === -1 && t > 0 && (a = t - 1), a >= 0 && (window.currentActiveIndex = a, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
block: "start" block: "start"
}), setTimeout(() => { }), setTimeout(() => {
b(); S();
}, 100)); }, 100));
} }
} }
function G() { function Q() {
if (window.currentActiveIndex < window.currentPageContainers.length - 1) { if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
let a = -1; let a = -1;
const e = []; const e = [];
@@ -1359,12 +1542,12 @@ function G() {
a === -1 && t < window.currentPageContainers.length - 1 && (a = t + 1), a >= 0 && a < window.currentPageContainers.length && (window.currentActiveIndex = a, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({ a === -1 && t < window.currentPageContainers.length - 1 && (a = t + 1), a >= 0 && a < window.currentPageContainers.length && (window.currentActiveIndex = a, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
block: "start" block: "start"
}), setTimeout(() => { }), setTimeout(() => {
b(); S();
}, 100)); }, 100));
} }
} }
function Y() { function ee() {
if (C()) { if (H()) {
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"
@@ -1383,7 +1566,7 @@ function Y() {
} }
} }
} }
function C() { function H() {
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;
@@ -1396,14 +1579,14 @@ function C() {
} }
return !1; return !1;
} }
function b() { 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 = C(), n = t.querySelector("i"); const i = H(), 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 U() { function te() {
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]) {
@@ -1415,54 +1598,54 @@ function U() {
title: document.title, title: document.title,
url: t url: t
}).catch((i) => { }).catch((i) => {
x(t, a); k(t, a);
}) : x(t, a); }) : k(t, a);
} }
function x(a, e) { function k(a, e) {
if (navigator.clipboard) if (navigator.clipboard)
navigator.clipboard.writeText(a).then(() => { navigator.clipboard.writeText(a).then(() => {
g(e, "Link kopiert!"); f(e, "Link kopiert!");
}).catch((t) => { }).catch((t) => {
g(e, "Kopieren fehlgeschlagen"); f(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");
g(e, i ? "Link kopiert!" : "Kopieren fehlgeschlagen"); f(e, i ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
g(e, "Kopieren fehlgeschlagen"); f(e, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(t); document.body.removeChild(t);
} }
} }
} }
function X() { function ie() {
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(() => {
g(a, "Zitation kopiert!"); f(a, "Zitation kopiert!");
}).catch((s) => { }).catch((s) => {
g(a, "Kopieren fehlgeschlagen"); f(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");
g(a, o ? "Zitation kopiert!" : "Kopieren fehlgeschlagen"); f(a, o ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
g(a, "Kopieren fehlgeschlagen"); f(a, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(s); document.body.removeChild(s);
} }
} }
} }
function g(a, e) { function f(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");
@@ -1491,7 +1674,7 @@ function g(a, e) {
}, 200); }, 200);
}, 2e3); }, 2e3);
} }
function Q(a, e, t = !1) { function ne(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}`;
@@ -1506,24 +1689,24 @@ function Q(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(() => {
g(e, "Link kopiert!"); f(e, "Link kopiert!");
}).catch((s) => { }).catch((s) => {
g(e, "Kopieren fehlgeschlagen"); f(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");
g(e, o ? "Link kopiert!" : "Kopieren fehlgeschlagen"); f(e, o ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
g(e, "Kopieren fehlgeschlagen"); f(e, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(s); document.body.removeChild(s);
} }
} }
} }
function _(a, e) { function se(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) {
@@ -1534,33 +1717,33 @@ function _(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(() => {
g(e, "Zitation kopiert!"); f(e, "Zitation kopiert!");
}).catch((l) => { }).catch((l) => {
g(e, "Kopieren fehlgeschlagen"); f(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");
g(e, c ? "Zitation kopiert!" : "Kopieren fehlgeschlagen"); f(e, c ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch { } catch {
g(e, "Kopieren fehlgeschlagen"); f(e, "Kopieren fehlgeschlagen");
} finally { } finally {
document.body.removeChild(l); document.body.removeChild(l);
} }
} }
} }
function L() { function M() {
Z(), window.addEventListener("scroll", function() { G(), window.addEventListener("scroll", function() {
clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => { clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => {
b(); S();
}, 50); }, 50);
}), document.addEventListener("keydown", function(a) { }), document.addEventListener("keydown", function(a) {
a.key === "Escape" && E(); a.key === "Escape" && q();
}); });
} }
function k() { function A() {
const a = window.location.pathname; const a = window.location.pathname;
document.querySelectorAll(".citation-link[data-citation-url]").forEach((t) => { document.querySelectorAll(".citation-link[data-citation-url]").forEach((t) => {
const i = t.getAttribute("data-citation-url"); const i = t.getAttribute("data-citation-url");
@@ -1577,7 +1760,7 @@ function k() {
n ? (t.classList.add("text-red-700", "pointer-events-none"), t.setAttribute("aria-current", "page")) : (t.classList.remove("text-red-700", "pointer-events-none"), t.removeAttribute("aria-current")); n ? (t.classList.add("text-red-700", "pointer-events-none"), t.setAttribute("aria-current", "page")) : (t.classList.remove("text-red-700", "pointer-events-none"), t.removeAttribute("aria-current"));
}); });
} }
function P() { function B() {
const a = window.location.pathname, e = document.body; const a = window.location.pathname, e = document.body;
e.classList.remove( e.classList.remove(
"page-akteure", "page-akteure",
@@ -1589,21 +1772,21 @@ function P() {
"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 = W; window.enlargePage = _;
window.closeModal = E; window.closeModal = q;
window.scrollToPreviousPage = J; window.scrollToPreviousPage = U;
window.scrollToNextPage = G; window.scrollToNextPage = Q;
window.scrollToBeilage = Y; window.scrollToBeilage = ee;
window.shareCurrentPage = U; window.shareCurrentPage = te;
window.generateCitation = X; window.generateCitation = ie;
window.copyPagePermalink = Q; window.copyPagePermalink = ne;
window.generatePageCitation = _; window.generatePageCitation = se;
P(); B();
k(); A();
document.querySelector(".newspaper-page-container") && L(); document.querySelector(".newspaper-page-container") && M();
let ee = function(a) { let oe = function(a) {
P(), k(), S(), setTimeout(() => { B(), A(), I(), setTimeout(() => {
document.querySelector(".newspaper-page-container") && L(); document.querySelector(".newspaper-page-container") && M();
}, 50); }, 50);
}; };
document.body.addEventListener("htmx:afterSettle", ee); document.body.addEventListener("htmx:afterSettle", oe);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -134,7 +134,7 @@
{{- /* Author prefix for colon format (place view) */ -}} {{- /* Author prefix for colon format (place view) */ -}}
{{- $colonPrefix := "" -}} {{- $colonPrefix := "" -}}
{{- if $useColonFormat -}} {{- if $useColonFormat -}}
{{- $colonPrefix = ":" -}} {{- $colonPrefix = ", " -}}
{{- else -}} {{- else -}}
{{- $colonPrefix = " mit " -}} {{- $colonPrefix = " mit " -}}
{{- end -}} {{- end -}}

View File

@@ -1,7 +1,7 @@
{{- /* Associated pieces section for place detail view */ -}} {{- /* Associated pieces section for place detail view */ -}}
<!-- Associated Pieces --> <!-- Associated Pieces -->
<div> <div class="mt-2">
<h2 class="text-xl font-semibold text-slate-800 mb-4"> <h2 class="font-bold mb-4">
<i class="ri-newspaper-line mr-2"></i><u class="decoration underline-offset-3">Verlinkte Beiträge</u> ({{ len .Pieces }}) <i class="ri-newspaper-line mr-2"></i><u class="decoration underline-offset-3">Verlinkte Beiträge</u> ({{ len .Pieces }})
</h2> </h2>
@@ -27,13 +27,13 @@
{{- end -}} {{- end -}}
{{- $sortedYears = sortStrings $sortedYears -}} {{- $sortedYears = sortStrings $sortedYears -}}
<div class="space-y-6 max-w-[85ch]"> <div class="columns-2 gap-6 hyphens-auto">
{{- range $year := $sortedYears -}} {{- range $year := $sortedYears -}}
{{- $yearPieces := index $piecesByYear $year -}} {{- $yearPieces := index $piecesByYear $year -}}
<!-- Year Header --> <!-- Year Header -->
<div> <div class="break-inside-avoid mb-6">
<h3 class="text-lg font-bold font-serif text-slate-800 mb-3">{{ $year }}</h3> <h3 class="font-bold font-serif text-slate-800 mb-1">{{ $year }}</h3>
<div class="space-y-1"> <div class="space-y-1">
{{- /* Group pieces by title within each year */ -}} {{- /* Group pieces by title within each year */ -}}
@@ -57,8 +57,8 @@
{{- end -}} {{- end -}}
{{- range $groupKey, $groupedItems := $groupedPieces -}} {{- range $groupKey, $groupedItems := $groupedPieces -}}
<div> <div class="break-inside-avoid">
<div class="pb-1 text-lg indent-4"> <div class="pb-1 indent-4">
{{- /* Use first piece for display text with colon format for places */ -}} {{- /* Use first piece for display text with colon format for places */ -}}
{{ template "_unified_piece_entry" (dict "Piece" (index $groupedItems 0) "CurrentActorID" "" "DisplayMode" "place" "ShowPlaceTags" false "UseColonFormat" true "ShowContinuation" false) }} {{ template "_unified_piece_entry" (dict "Piece" (index $groupedItems 0) "CurrentActorID" "" "DisplayMode" "place" "ShowPlaceTags" false "UseColonFormat" true "ShowContinuation" false) }}
@@ -78,7 +78,8 @@
{{- if gt (len $firstGroupItem.IssueRefs) 1 -}} {{- if gt (len $firstGroupItem.IssueRefs) 1 -}}
{{ " " }}<div class="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 {{ " " }}<div class="inline-flex items-center gap-1 px-2 py-1 bg-blue-50
hover:bg-blue-100 text-blue-700 hover:text-blue-800 border border-blue-200 hover:bg-blue-100 text-blue-700 hover:text-blue-800 border border-blue-200
hover:border-blue-300 rounded text-xs font-medium transition-colors duration-200"> hover:border-blue-300 rounded text-xs font-medium transition-colors duration-200
indent-0">
<i class="ri-file-copy-2-line text-xs"></i> <i class="ri-file-copy-2-line text-xs"></i>
<a href="{{ GetPieceURL $firstGroupItem.ID }}" class=""> <a href="{{ GetPieceURL $firstGroupItem.ID }}" class="">
Ganzer Beitrag Ganzer Beitrag

View File

@@ -1,4 +1,4 @@
{{- /* Fragment with specific class for HTMX selection */ -}} {{- /* Fragment with specific class for HTMX selection */ -}}
<div class="place-fragment-content p-4 border-t border-slate-200 bg-slate-50"> <div class="place-fragment-content p-4 border-t border-slate-200 bg-slate-50 text-base">
{{ template "_place_pieces" .model.SelectedPlace }} {{ template "_place_pieces" .model.SelectedPlace }}
</div> </div>

View File

@@ -1,10 +1,10 @@
{{- /* Places overview page body */ -}} {{- /* Places overview page body */ -}}
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-7 gap-8">
{{- /* Main content - Places list */ -}} {{- /* Main content - Places list */ -}}
<div class="lg:col-span-3"> <div class="lg:col-span-5">
{{- /* Places list */ -}} {{- /* Places list */ -}}
{{ if .model.Places }} {{ if .model.Places }}
<div class="bg-white border border-slate-200 rounded-lg mt-6"> <div class="bg-white mt-6">
{{ range $placeID := .model.Sorted }} {{ range $placeID := .model.Sorted }}
{{ $place := index $.model.Places $placeID }} {{ $place := index $.model.Places $placeID }}
{{ $pieceCount := index $.model.PlacePieceCounts $placeID }} {{ $pieceCount := index $.model.PlacePieceCounts $placeID }}
@@ -20,7 +20,7 @@
<div class="block p-4 hover:bg-slate-50 transition-colors duration-200 cursor-pointer"> <div class="block p-4 hover:bg-slate-50 transition-colors duration-200 cursor-pointer">
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h3 class="font-medium text-slate-800 mb-1 truncate"> <h3 class="font-bold text-slate-800 mb-1 truncate">
{{ if $place.Names }} {{ if $place.Names }}
{{ index $place.Names 0 }} {{ index $place.Names 0 }}
{{ else }} {{ else }}
@@ -41,6 +41,24 @@
{{ end }} {{ end }}
</div> </div>
<div class="flex-shrink-0 flex items-center gap-2"> <div class="flex-shrink-0 flex items-center gap-2">
<!-- External links -->
{{ if ne $geonames nil }}
{{ if ne $geonames.WikipediaURL "" }}
<a href="https://{{ $geonames.WikipediaURL }}" target="_blank" class="hover:opacity-80 transition-opacity no-underline" title="Wikipedia" onclick="event.stopPropagation()">
<img src="/assets/wikipedia.png" alt="Wikipedia" class="w-5 h-5">
</a>
{{ end }}
{{ if and (ne $geonames.Lat "") (ne $geonames.Lng "") }}
<a href="https://www.openstreetmap.org/?mlat={{ $geonames.Lat }}&mlon={{ $geonames.Lng }}&zoom=12" target="_blank" class="hover:opacity-80 transition-opacity no-underline" title="OpenStreetMap" onclick="event.stopPropagation()">
<i class="ri-map-pin-line text-base text-green-600"></i>
</a>
{{ end }}
{{ end }}
{{ if $place.Geo }}
<a href="{{ $place.Geo }}" target="_blank" class="hover:opacity-80 transition-opacity no-underline" title="Geonames" onclick="event.stopPropagation()">
<i class="ri-global-line text-base text-blue-600"></i>
</a>
{{ end }}
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-slate-100 text-slate-700 rounded"> <span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-slate-100 text-slate-700 rounded">
{{ $pieceCount }} {{ $pieceCount }}
</span> </span>
@@ -65,16 +83,17 @@
</div> </div>
{{- /* Sidebar */ -}} {{- /* Sidebar */ -}}
<div class="lg:col-span-1 sticky top-0 self-start"> <div class="lg:col-span-2 sticky top-0 self-start">
<div class="bg-slate-50 p-6 filter-sidebar"> {{- /* Top Section - Title, Description, Search */ -}}
<h1 class="text-2xl font-bold text-slate-800 mb-4">Orte</h1> <div class="bg-slate-50 p-6 border-b border-slate-200 filter-sidebar">
<h1 class="text-2xl font-bold text-slate-800 mb-2">Orte</h1>
<p class="text-slate-600 mb-6"> <p class="text-slate-600 mb-6">
Verzeichnis aller in der Zeitung erwähnten Orte und Lokalitäten Verzeichnis aller in der Zeitung erwähnten Orte und Lokalitäten
</p> </p>
{{- /* Search Filter */ -}} {{- /* Search Filter */ -}}
<div class="mb-4"> <div class="flex items-center gap-4">
<div class="flex-1">
<generic-filter <generic-filter
placeholder="Ortsnamen eingeben..." placeholder="Ortsnamen eingeben..."
item-selector="[data-place-name]" item-selector="[data-place-name]"
@@ -84,30 +103,14 @@
item-type-singular="Ort"> item-type-singular="Ort">
</generic-filter> </generic-filter>
</div> </div>
<div class="flex items-center text-sm font-bold text-slate-700 whitespace-nowrap" data-filter-count></div>
<div class="text-sm text-slate-700 mb-4" data-filter-count>
Alle Orte ({{ len .model.Places }})
</div> </div>
</div> </div>
{{- /* Statistics */ -}} {{- /* Bottom Section - Map */ -}}
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mt-4"> <div class="bg-white p-6">
<div class="text-sm text-slate-700 space-y-2"> <places-map data-places="{{ .model.PlacesJSON }}" class="w-full"></places-map>
<div class="flex justify-between">
<span>Orte gesamt:</span>
<span class="font-medium">{{ len .model.Places }}</span>
</div>
<div class="flex justify-between">
<span>Beiträge mit Ort:</span>
<span class="font-medium">{{ .model.TotalPiecesWithPlaces }}</span>
</div>
{{- if .model.SelectedPlace -}}
<div class="flex justify-between">
<span>Beiträge hier:</span>
<span class="font-medium">{{ len .model.SelectedPlace.Pieces }}</span>
</div>
{{- end -}}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -37,7 +37,6 @@ export class GenericFilter extends HTMLElement {
render() { render() {
this.innerHTML = ` this.innerHTML = `
<div class="mb-6">
<input <input
type="text" type="text"
id="generic-search" id="generic-search"
@@ -45,7 +44,6 @@ export class GenericFilter extends HTMLElement {
autocomplete="off" autocomplete="off"
class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400" class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
> >
</div>
`; `;
} }
@@ -79,6 +77,8 @@ export class GenericFilter extends HTMLElement {
if (this.countElement) { if (this.countElement) {
this.originalCount = this.itemCards.length; this.originalCount = this.itemCards.length;
// Initially hide the count element since no search is active
this.countElement.style.display = 'none';
} }
} }
@@ -130,15 +130,19 @@ export class GenericFilter extends HTMLElement {
if (!this.countElement) return; if (!this.countElement) return;
if (searchTerm === '') { if (searchTerm === '') {
// Show original count when no search // Hide count when no search is active
this.countElement.textContent = `Alle ${this.itemType} (${this.originalCount})`; this.countElement.style.display = 'none';
} else if (visibleCount === 0) {
// Show no results message
this.countElement.textContent = `Keine ${this.itemType} gefunden für "${searchTerm}"`;
} else { } else {
// Show filtered count // Show count element when searching
const itemTypeText = visibleCount === 1 ? this.itemTypeSingular : this.itemType; this.countElement.style.display = '';
this.countElement.textContent = `${visibleCount} von ${this.originalCount} ${itemTypeText}`;
if (visibleCount === 0) {
// Show zero for no results
this.countElement.textContent = '(0)';
} else {
// Show just the filtered count number in parentheses
this.countElement.textContent = `(${visibleCount})`;
}
} }
} }
} }

View File

@@ -133,10 +133,26 @@ export class PlaceAccordion extends HTMLElement {
connectedCallback() { connectedCallback() {
this.setupAccordion(); this.setupAccordion();
this.setupEventListeners(); this.setupEventListeners();
this.updateBorders();
this.setupMapEventListeners();
this.setupHoverEvents();
} }
disconnectedCallback() { disconnectedCallback() {
this.cleanupEventListeners(); this.cleanupEventListeners();
this.cleanupMapEventListeners();
}
cleanupMapEventListeners() {
// Clean up map event listeners
document.removeEventListener('place-map-clicked', this.handleMapClick.bind(this));
// Clean up hover event listeners
const headerDiv = this.querySelector('.cursor-pointer');
if (headerDiv) {
headerDiv.removeEventListener('mouseenter', this.handleHeadingHover.bind(this));
headerDiv.removeEventListener('mouseleave', this.handleHeadingLeave.bind(this));
}
} }
setupAccordion() { setupAccordion() {
@@ -180,6 +196,57 @@ export class PlaceAccordion extends HTMLElement {
this.removeEventListener('click', this.handleClick.bind(this)); this.removeEventListener('click', this.handleClick.bind(this));
} }
setupMapEventListeners() {
// Listen for map click events
document.addEventListener('place-map-clicked', this.handleMapClick.bind(this));
}
setupHoverEvents() {
// Find the clickable header div within this accordion
const headerDiv = this.querySelector('.cursor-pointer');
if (headerDiv) {
headerDiv.addEventListener('mouseenter', this.handleHeadingHover.bind(this));
headerDiv.addEventListener('mouseleave', this.handleHeadingLeave.bind(this));
}
}
handleHeadingHover() {
const placeId = this.getAttribute('data-place-id');
if (placeId) {
// Emit event to show tooltip on map
const showTooltipEvent = new CustomEvent('place-heading-hover', {
detail: { placeId, action: 'show' },
bubbles: true
});
document.dispatchEvent(showTooltipEvent);
}
}
handleHeadingLeave() {
const placeId = this.getAttribute('data-place-id');
if (placeId) {
// Emit event to hide tooltip on map
const hideTooltipEvent = new CustomEvent('place-heading-hover', {
detail: { placeId, action: 'hide' },
bubbles: true
});
document.dispatchEvent(hideTooltipEvent);
}
}
handleMapClick(event) {
const clickedPlaceId = event.detail.placeId;
const myPlaceId = this.getAttribute('data-place-id');
// If this accordion matches the clicked place, expand it
if (clickedPlaceId === myPlaceId && !this.isExpanded) {
// Add a small delay to allow scrolling to complete first
setTimeout(() => {
this.expand();
}, 800); // Delay matches scroll animation + highlight effect
}
}
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]');
@@ -203,6 +270,7 @@ export class PlaceAccordion extends HTMLElement {
this.isExpanded = true; this.isExpanded = true;
this.updateChevron(); this.updateChevron();
this.updateBorders();
const contentContainer = this.querySelector('[data-content]'); const contentContainer = this.querySelector('[data-content]');
if (!contentContainer) return; if (!contentContainer) return;
@@ -219,6 +287,7 @@ export class PlaceAccordion extends HTMLElement {
collapse() { collapse() {
this.isExpanded = false; this.isExpanded = false;
this.updateChevron(); this.updateChevron();
this.updateBorders();
const contentContainer = this.querySelector('[data-content]'); const contentContainer = this.querySelector('[data-content]');
if (contentContainer) { if (contentContainer) {
@@ -268,8 +337,437 @@ export class PlaceAccordion extends HTMLElement {
} }
} }
} }
updateBorders() {
if (this.isExpanded) {
// When expanded: remove border from header, add border to whole component
this.classList.add('border-b', 'border-slate-100');
} else {
// When collapsed: add border to component (for separation between items)
this.classList.add('border-b', 'border-slate-100');
}
// Remove border from last item if it's the last child
const isLastChild = !this.nextElementSibling;
if (isLastChild) {
this.classList.remove('border-b');
}
}
}
/**
* Places Map Web Component
* Embeds an SVG map with plotted geographical places
*/
export class PlacesMap extends HTMLElement {
constructor() {
super();
this.places = [];
this.mapElement = null;
this.pointsContainer = null;
this.intersectionObserver = null;
this.mapPoints = new Map(); // Map of placeId -> point element
this.tooltip = null;
this.tooltipTimeout = null;
}
connectedCallback() {
this.parseData();
this.render();
this.initializeMap();
// Delay scrollspy initialization to ensure DOM is ready
setTimeout(() => {
this.initializeScrollspy();
}, 200);
this.setupHeadingHoverListener();
}
parseData() {
try {
const placesData = this.dataset.places;
if (placesData) {
this.places = JSON.parse(placesData);
}
} catch (error) {
console.error('Failed to parse places data:', error);
this.places = [];
}
}
render() {
this.innerHTML = `
<div class="map-container relative w-full aspect-[5/7] overflow-hidden rounded border border-slate-200 bg-slate-100">
<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">
<div class="points-container absolute top-0 left-0 w-full h-full"></div>
</div>
<!-- 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>
`;
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;
}
// Map extent constants (same as example)
const MAP_EXTENT_METERS = { xmin: 2555000, ymin: 1350000, xmax: 7405000, ymax: 5500000 };
const PROJECTION_CENTER = { lon: 10, lat: 52 };
// Convert lat/lng to percent position
const convertLatLngToPercent = (lat, lng) => {
const R = 6371000;
const FE = 4321000;
const FN = 3210000;
const lon_0_rad = PROJECTION_CENTER.lon * Math.PI / 180;
const lat_0_rad = PROJECTION_CENTER.lat * Math.PI / 180;
const lon_rad = lng * 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 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 finalX = x_proj + FE;
const finalY = y_proj + FN;
const mapWidthMeters = MAP_EXTENT_METERS.xmax - MAP_EXTENT_METERS.xmin;
const mapHeightMeters = MAP_EXTENT_METERS.ymax - MAP_EXTENT_METERS.ymin;
const xPercent = (finalX - MAP_EXTENT_METERS.xmin) / mapWidthMeters * 100;
const yPercent = (MAP_EXTENT_METERS.ymax - finalY) / mapHeightMeters * 100;
return { x: xPercent, y: yPercent };
};
// Create points and track positions
const pointPositions = [];
this.places.forEach(place => {
if (place.lat && place.lng) {
const lat = parseFloat(place.lat);
const lng = parseFloat(place.lng);
const position = convertLatLngToPercent(lat, lng);
// Only add points that are within the visible map area
if (position.x >= 0 && position.x <= 100 && position.y >= 0 && position.y <= 100) {
pointPositions.push(position);
const point = document.createElement('div');
point.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';
point.style.left = `${position.x}%`;
point.style.top = `${position.y}%`;
point.style.transformOrigin = 'center';
const tooltipText = `${place.name}${place.toponymName && place.toponymName !== place.name ? ` (${place.toponymName})` : ''}`;
point.dataset.placeId = place.id;
point.dataset.tooltipText = tooltipText;
// Add hover and click event listeners
point.addEventListener('mouseenter', (e) => this.showTooltip(e));
point.addEventListener('mouseleave', () => this.hideTooltip());
point.addEventListener('mousemove', (e) => this.updateTooltipPosition(e));
point.addEventListener('click', (e) => this.scrollToPlace(e));
this.pointsContainer.appendChild(point);
// Store reference to point for scrollspy
this.mapPoints.set(place.id, point);
}
}
});
// Auto-zoom to fit all points
if (pointPositions.length > 0) {
this.autoZoomToPoints(pointPositions);
}
}
autoZoomToPoints(pointPositions) {
// Calculate bounding box of all points
let minX = 100, maxX = 0, minY = 100, maxY = 0;
pointPositions.forEach(pos => {
if (pos.x < minX) minX = pos.x;
if (pos.x > maxX) maxX = pos.x;
if (pos.y < minY) minY = pos.y;
if (pos.y > maxY) maxY = pos.y;
});
// Add 5% padding
const width = maxX - minX;
const height = maxY - minY;
const paddingX = width * 0.05;
const paddingY = height * 0.05;
const paddedMinX = Math.max(0, minX - paddingX);
const paddedMaxX = Math.min(100, maxX + paddingX);
const paddedMinY = Math.max(0, minY - paddingY);
const paddedMaxY = Math.min(100, maxY + paddingY);
const newWidth = paddedMaxX - paddedMinX;
const newHeight = paddedMaxY - paddedMinY;
// Maintain 5:7 aspect ratio
const targetAspectRatio = 5 / 7;
const pointsAspectRatio = newWidth / newHeight;
let finalViewBox = { x: paddedMinX, y: paddedMinY, width: newWidth, height: newHeight };
if (pointsAspectRatio > targetAspectRatio) {
const newTargetHeight = newWidth / targetAspectRatio;
finalViewBox.y = paddedMinY - (newTargetHeight - newHeight) / 2;
finalViewBox.height = newTargetHeight;
} else {
const newTargetWidth = newHeight * targetAspectRatio;
finalViewBox.x = paddedMinX - (newTargetWidth - newWidth) / 2;
finalViewBox.width = newTargetWidth;
}
// Apply transformation
const scale = 100 / finalViewBox.width;
const translateX = -finalViewBox.x;
const translateY = -finalViewBox.y;
const transformValue = `scale(${scale}) translate(${translateX}%, ${translateY}%)`;
const transformWrapper = this.querySelector('.transform-wrapper');
if (transformWrapper) {
transformWrapper.style.transform = transformValue;
}
}
initializeScrollspy() {
// Find all place containers in the places list
const placeContainers = document.querySelectorAll('place-accordion[data-place-id]');
if (!placeContainers.length) return;
// First, ensure all points start in inactive state
this.mapPoints.forEach(point => {
this.setPointInactive(point);
});
// Create intersection observer
this.intersectionObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
const placeId = entry.target.getAttribute('data-place-id');
const mapPoint = this.mapPoints.get(placeId);
if (mapPoint) {
if (entry.isIntersecting) {
// Activate the point when place is in view
this.setPointActive(mapPoint);
} else {
// Deactivate the point when place is out of view
this.setPointInactive(mapPoint);
}
}
});
},
{
// Trigger when element enters viewport
threshold: 0.1,
// No root margin for precise detection
rootMargin: '0px'
}
);
// Observe all place containers
placeContainers.forEach(container => {
this.intersectionObserver.observe(container);
});
// Force an immediate check by triggering a scroll event
setTimeout(() => {
// Manually trigger intersection calculation
placeContainers.forEach(container => {
const rect = container.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
const placeId = container.getAttribute('data-place-id');
const mapPoint = this.mapPoints.get(placeId);
if (mapPoint && isVisible) {
this.setPointActive(mapPoint);
}
});
}, 50);
}
setPointActive(point) {
// 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:z-30';
}
setPointInactive(point) {
// Inactive state: small light red dots, no opacity effects
point.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(event) {
const point = event.target;
const tooltipText = point.dataset.tooltipText;
if (this.tooltip && tooltipText) {
// Clear any existing timeout
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout);
}
// Set tooltip position immediately but keep it invisible
this.tooltip.textContent = tooltipText;
this.updateTooltipPosition(event);
// Show tooltip after 1 second delay
this.tooltipTimeout = setTimeout(() => {
this.tooltip.classList.remove('opacity-0');
this.tooltip.classList.add('opacity-100');
}, 1000);
}
}
hideTooltip() {
// Clear any pending tooltip timeout
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
}
if (this.tooltip) {
this.tooltip.classList.remove('opacity-100');
this.tooltip.classList.add('opacity-0');
}
}
updateTooltipPosition(event) {
if (!this.tooltip) return;
const mapRect = this.mapElement.getBoundingClientRect();
const x = event.clientX - mapRect.left;
const y = event.clientY - mapRect.top;
// Position tooltip relative to the map container
this.tooltip.style.left = `${x}px`;
this.tooltip.style.top = `${y}px`;
}
scrollToPlace(event) {
const placeId = event.target.dataset.placeId;
if (!placeId) return;
// Emit custom event for place selection
const placeSelectedEvent = new CustomEvent('place-map-clicked', {
detail: { placeId },
bubbles: true
});
this.dispatchEvent(placeSelectedEvent);
// Find the corresponding place container in the list
const placeContainer = document.querySelector(`place-accordion[data-place-id="${placeId}"]`);
if (placeContainer) {
// Smooth scroll to the place container
placeContainer.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
// Optional: Add a brief highlight effect
placeContainer.style.transition = 'background-color 0.3s ease';
placeContainer.style.backgroundColor = 'rgb(248 250 252)';
setTimeout(() => {
placeContainer.style.backgroundColor = '';
}, 1000);
}
}
setupHeadingHoverListener() {
// Listen for heading hover events from place accordions
document.addEventListener('place-heading-hover', this.handleHeadingHoverEvent.bind(this));
}
handleHeadingHoverEvent(event) {
const { placeId, action } = event.detail;
const mapPoint = this.mapPoints.get(placeId);
if (!mapPoint) return;
if (action === 'show') {
// Give the point a subtle highlight by making it larger immediately
mapPoint.classList.remove('w-1', 'h-1', 'w-1.5', 'h-1.5');
mapPoint.classList.add('w-2', 'h-2');
mapPoint.style.zIndex = '25';
// Show tooltip for the corresponding map point after delay
const tooltipText = mapPoint.dataset.tooltipText;
if (this.tooltip && tooltipText) {
// Clear any existing timeout
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout);
}
// Set tooltip position immediately but keep it invisible
this.tooltip.textContent = tooltipText;
// Position tooltip at the map point location
const pointRect = mapPoint.getBoundingClientRect();
const mapRect = this.mapElement.getBoundingClientRect();
const x = pointRect.left - mapRect.left + (pointRect.width / 2);
const y = pointRect.top - mapRect.top + (pointRect.height / 2);
this.tooltip.style.left = `${x}px`;
this.tooltip.style.top = `${y}px`;
// Show tooltip after 1 second delay
this.tooltipTimeout = setTimeout(() => {
this.tooltip.classList.remove('opacity-0');
this.tooltip.classList.add('opacity-100');
}, 1000);
}
} else if (action === 'hide') {
// Clear any pending tooltip timeout
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
}
// Hide tooltip
if (this.tooltip) {
this.tooltip.classList.remove('opacity-100');
this.tooltip.classList.add('opacity-0');
}
// Remove point highlight - restore original size based on current state
mapPoint.classList.remove('w-2', 'h-2');
// Check if this point is currently active or inactive
if (mapPoint.className.includes('bg-red-500')) {
// Active point
mapPoint.classList.add('w-1.5', 'h-1.5');
} else {
// Inactive point
mapPoint.classList.add('w-1', 'h-1');
}
mapPoint.style.zIndex = ''; // Reset to default
}
}
disconnectedCallback() {
// Clean up intersection observer
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
}
// Clean up tooltip timeout
if (this.tooltipTimeout) {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
}
// Clean up heading hover listener
document.removeEventListener('place-heading-hover', this.handleHeadingHoverEvent.bind(this));
}
} }
// 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);