Orte + Filter; -Alpine

This commit is contained in:
Simon Martens
2025-09-24 22:33:38 +02:00
parent 209da5b509
commit c19ba1e3dc
11 changed files with 1227 additions and 497 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -22,7 +22,6 @@
}
});
</script>
<script src="/assets/js/alpine.min.js" defer></script>
<link rel="stylesheet" type="text/css" href="/assets/css/fonts.css?v={{ .timestamp }}" />
<link rel="stylesheet" type="text/css" href="/assets/style.css?v={{ .timestamp }}" />

View File

@@ -1,13 +1,6 @@
<div class="flex flex-row justify-center mt-12 mb-8">
<!-- Schnellfilter Button -->
<div>
<button
id="filter-toggle"
class="mr-2 text-lg border px-4 h-full hover:bg-slate-200 transition-colors cursor-pointer"
title="Schnellfilter öffnen/schließen">
<i class="ri-filter-2-line"></i> <div class="inline-block text-lg">Schnellauswahl</div>
</button>
</div>
<schnellauswahl-button></schnellauswahl-button>
<div class="w-6/12">
<input
@@ -22,29 +15,6 @@
hx-target="main" />
</div>
<div x-data="{ open: false }">
<button
class="ml-2 text-2xl border px-4 h-full hover:bg-slate-200 transition-colors cursor-pointer"
:class="open? 'open bg-slate-200' : 'closed'"
@click="open = !open">
<i class="ri-menu-line"></i>
</button>
<div :class="open? 'open' : 'closed'" x-show="open" class="absolute bg-slate-50 px-5 py-3 z-50">
<div>
<div>Übersicht nach</div>
<div class="ml-2 flex flex-col gap-y-2 mt-2">
<a href="/">Jahrgängen</a>
<a href="/akteure/a">Personen</a>
<a href="/kategorie/">Betragsarten</a>
<a href="/ort/">Orten</a>
</div>
</div>
<div class="flex flex-col gap-y-2 mt-2">
<a href="/edition/">Geschichte &amp; Edition der KGPZ</a>
<a href="/zitation/">Zitation</a>
<a href="/kontakt/">Kontakt</a>
</div>
</div>
</div>
<navigation-menu></navigation-menu>
</div>

View File

@@ -0,0 +1,203 @@
{{- $piece := .Piece -}}
{{- $currentActorID := .CurrentActorID -}}
{{- $categoryFlags := GetCategoryFlags $piece -}}
{{- $place := "" -}}
{{- if $piece.PlaceRefs -}}
{{- $placeObj := GetPlace (index $piece.PlaceRefs 0).Ref -}}
{{- if gt (len $placeObj.Names) 0 -}}
{{- $place = index $placeObj.Names 0 -}}
{{- end -}}
{{- end -}}
{{- $title := "" -}}
{{- if $piece.Title -}}
{{- $title = index $piece.Title 0 -}}
{{- else if $piece.Incipit -}}
{{- $title = index $piece.Incipit 0 -}}
{{- end -}}
{{- $workTitle := "" -}}
{{- $workAuthorName := "" -}}
{{- $workAuthorID := "" -}}
{{- if $piece.WorkRefs -}}
{{- $work := GetWork (index $piece.WorkRefs 0).Ref -}}
{{- if $work.PreferredTitle -}}
{{- $workTitle = $work.PreferredTitle -}}
{{- else if $work.Citation.Title -}}
{{- $workTitle = $work.Citation.Title -}}
{{- end -}}
{{- if $work.AgentRefs -}}
{{- range $workAgentRef := $work.AgentRefs -}}
{{- if (or (eq $workAgentRef.Category "") (eq $workAgentRef.Category "autor")) -}}
{{- $workAgent := GetAgent $workAgentRef.Ref -}}
{{- if and $workAgent (gt (len $workAgent.Names) 0) -}}
{{- $workAuthorName = index $workAgent.Names 0 -}}
{{- $workAuthorID = $workAgentRef.Ref -}}
{{- break -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* Build category list */ -}}
{{- $categories := slice -}}
{{- if $categoryFlags.Rezension -}}
{{- $categories = append $categories "Rezension" -}}
{{- end -}}
{{- if $categoryFlags.Gedicht -}}
{{- $categories = append $categories "Gedicht" -}}
{{- end -}}
{{- if $categoryFlags.Aufsatz -}}
{{- $categories = append $categories "Aufsatz" -}}
{{- end -}}
{{- if $categoryFlags.Theaterkritik -}}
{{- $categories = append $categories "Theaterkritik" -}}
{{- end -}}
{{- if $categoryFlags.Brief -}}
{{- $categories = append $categories "Brief" -}}
{{- end -}}
{{- if $categoryFlags.Erzaehlung -}}
{{- $categories = append $categories "Erzählung" -}}
{{- end -}}
{{- if $categoryFlags.Kommentar -}}
{{- $categories = append $categories "Kommentar" -}}
{{- end -}}
{{- if $categoryFlags.Uebersetzung -}}
{{- $categories = append $categories "Übersetzung" -}}
{{- end -}}
{{- if $categoryFlags.Auszug -}}
{{- $categories = append $categories "Auszug" -}}
{{- end -}}
{{- if $categoryFlags.Replik -}}
{{- $categories = append $categories "Replik" -}}
{{- end -}}
{{- if $categoryFlags.Lokalnachrichten -}}
{{- $categories = append $categories "Lokalnachrichten" -}}
{{- end -}}
{{- if $categoryFlags.Lotterie -}}
{{- $categories = append $categories "Lotterie" -}}
{{- end -}}
{{- if $categoryFlags.Nachruf -}}
{{- $categories = append $categories "Nachruf" -}}
{{- end -}}
{{- if $categoryFlags.Weltnachrichten -}}
{{- $categories = append $categories "Weltnachrichten" -}}
{{- end -}}
{{- if $categoryFlags.EinkommendeFremde -}}
{{- $categories = append $categories "Einkommende Fremde" -}}
{{- end -}}
{{- if $categoryFlags.Wechselkurse -}}
{{- $categories = append $categories "Wechselkurse" -}}
{{- end -}}
{{- if $categoryFlags.Buecher -}}
{{- $categories = append $categories "Bücher" -}}
{{- end -}}
{{- if $categoryFlags.Lokalanzeigen -}}
{{- $categories = append $categories "Lokalanzeigen" -}}
{{- end -}}
{{- if $categoryFlags.Vorladung -}}
{{- $categories = append $categories "Vorladung" -}}
{{- end -}}
{{- if $categoryFlags.GelehrteNachrichten -}}
{{- $categories = append $categories "Gelehrte Nachrichten" -}}
{{- end -}}
{{- if $categoryFlags.Anzeige -}}
{{- $categories = append $categories "Anzeige" -}}
{{- end -}}
{{- if $categoryFlags.Proklamation -}}
{{- $categories = append $categories "Proklamation" -}}
{{- end -}}
{{- if $categoryFlags.Desertionsliste -}}
{{- $categories = append $categories "Desertionsliste" -}}
{{- end -}}
{{- if $categoryFlags.Notenblatt -}}
{{- $categories = append $categories "Notenblatt" -}}
{{- end -}}
{{- if $categoryFlags.Vorlesungsverzeichnis -}}
{{- $categories = append $categories "Vorlesungsverzeichnis" -}}
{{- end -}}
{{- if $categoryFlags.Abbildung -}}
{{- $categories = append $categories "Abbildung" -}}
{{- end -}}
{{- if $categoryFlags.Ineigenersache -}}
{{- $categories = append $categories "In eigener Sache" -}}
{{- end -}}
{{- if $categoryFlags.Provinienz -}}
{{- $categories = append $categories "Provinienz" -}}
{{- end -}}
{{- /* Display category combination */ -}}
{{- $categoryName := "" -}}
{{- if eq (len $categories) 0 -}}
{{- $categoryName = "Beitrag" -}}
{{- else -}}
{{- $sortedCategories := sortStrings $categories -}}
{{- $categoryName = joinWithUnd $sortedCategories -}}
{{- end -}}
{{- /* Generate piece descriptions */ -}}
{{- if has $categories "Rezension" -}}
{{- /* Collect all additional authors (not current actor) */ -}}
{{- $additionalAuthors := slice -}}
{{- $currentAuthorFound := false -}}
{{- range $agentref := $piece.AgentRefs -}}
{{- if (or (eq $agentref.Category "") (eq $agentref.Category "autor")) -}}
{{- $agent := GetAgent $agentref.Ref -}}
{{- if and $agent (gt (len $agent.Names) 0) -}}
{{- if ne $agentref.Ref $currentActorID -}}
{{- $additionalAuthors = append $additionalAuthors $agent -}}
{{- else -}}
{{- $currentAuthorFound = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* Display review with colon instead of "mit" for place view */ -}}
{{- if $additionalAuthors -}}
{{ range $i, $author := $additionalAuthors }}{{- if gt $i 0 }} und {{ end }}<a href="/akteure/{{ $author.ID }}" class="text-slate-700 hover:text-slate-900 underline decoration-slate-400 hover:decoration-slate-600">{{ index $author.Names 0 }}</a>{{ end }}: {{ $categoryName }} von:
{{- else if $currentAuthorFound -}}
{{ $categoryName }} von:
{{- else -}}
{{ $categoryName }} von:
{{- end -}}
{{ if $workAuthorName }}
<a href="/akteure/{{ $workAuthorID }}" class="text-slate-700 hover:text-slate-900 underline decoration-slate-400 hover:decoration-slate-600">{{ $workAuthorName }}</a>,
{{ end }}
{{ if $workTitle }}
<em>{{ $workTitle }}</em>
{{ else if $title }}
<em>{{ $title }}</em>
{{ else }}
[Werk unbekannt]
{{ end }}
{{- else -}}
{{- /* Collect all additional authors (not current actor) */ -}}
{{- $additionalAuthors := slice -}}
{{- $currentAuthorFound := false -}}
{{- range $agentref := $piece.AgentRefs -}}
{{- if (or (eq $agentref.Category "") (eq $agentref.Category "autor")) -}}
{{- $agent := GetAgent $agentref.Ref -}}
{{- if and $agent (gt (len $agent.Names) 0) -}}
{{- if ne $agentref.Ref $currentActorID -}}
{{- $additionalAuthors = append $additionalAuthors $agent -}}
{{- else -}}
{{- $currentAuthorFound = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* Display with colon instead of "mit" for place view */ -}}
{{- if $additionalAuthors -}}
{{ range $i, $author := $additionalAuthors }}{{- if gt $i 0 }} und {{ end }}<a href="/akteure/{{ $author.ID }}" class="text-slate-700 hover:text-slate-900 underline decoration-slate-400 hover:decoration-slate-600">{{ index $author.Names 0 }}</a>{{ end }}: {{ $categoryName }}{{ if $title }}: <em>{{ $title }}</em>{{ end }}
{{- else if $currentAuthorFound -}}
{{ $categoryName }}{{ if $title }}: <em>{{ $title }}</em>{{ end }}
{{- else -}}
{{ $categoryName }}{{ if $title }}: <em>{{ $title }}</em>{{ else if eq $categoryName "Beitrag" }} ohne Titel{{ end }}
{{- end -}}
{{- end -}}

View File

@@ -1,5 +1,5 @@
<div class="flex flex-row justify-center gap-4" id="filter">
<div class="bg-white border border-slate-200 rounded-lg shadow-sm p-4 w-full max-w-md">
<div class="flex flex-row justify-center gap-4 h-96" id="filter">
<div class="p-4 w-full max-w-md flex flex-col h-full">
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
<i class="ri-calendar-line text-slate-600"></i>
Auswahl nach Datum, Nummer od. Seite
@@ -11,7 +11,7 @@
<!-- Year Selection -->
<div class="flex items-center gap-2 mb-4">
<label for="year-select" class="text-sm text-slate-600 w-12 hidden">Jahr wählen...</label>
<select id="year-select" class="tabular-nums flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400" style="max-height: 200px; overflow-y: auto;">
<select id="year-select" class="tabular-nums flex-1 px-2 py-1 border border-slate-300 rounded text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400" style="max-height: 200px; overflow-y: auto;">
<option value="">Jahr wählen</option>
{{ range $year := .AvailableYears }}
<option value="{{ $year }}">{{ $year }}</option>
@@ -21,10 +21,10 @@
<!-- Ausgabe Selection - Two Selects -->
<div class="flex items-center gap-2">
<select id="issue-number-select" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed">
<select id="issue-number-select" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed">
<option value="">Nr.</option>
</select>
<select id="issue-date-select" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed">
<select id="issue-date-select" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed">
<option value="">Datum</option>
</select>
</div>
@@ -39,7 +39,7 @@
<!-- Page Input -->
<div class="flex items-center gap-2">
<label for="page-input" class="text-sm text-slate-600 w-12 hidden"> oder Seite eingeben...</label>
<input type="number" id="page-input" min="1" placeholder="Seite eingeben" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed disabled:text-slate-500">
<input type="number" id="page-input" min="1" placeholder="Seite eingeben" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed disabled:text-slate-500">
</div>
<!-- Page Jump Button -->
@@ -54,22 +54,22 @@
</year-jump-filter>
</div>
<div class="bg-white border border-slate-200 rounded-lg shadow-sm p-4 w-full max-w-md">
<div class="p-4 w-full max-w-md flex flex-col h-full">
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
<i class="ri-user-line text-slate-600"></i>
Auswahl nach Person
</h3>
<!-- Person Jump Filter -->
<person-jump-filter>
<div class="space-y-3">
<person-jump-filter class="flex-1 flex flex-col min-h-0">
<div class="space-y-3 flex flex-col h-full">
<div class="flex items-center gap-2">
<label for="person-search" class="hidden text-sm text-slate-600 w-16">Filter</label>
<input
type="text"
id="person-search"
placeholder="Name oder Lebensdaten eingeben..."
class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
>
</div>
@@ -82,7 +82,7 @@
<label for="authors-only" class="text-sm text-slate-600">Nur Autor:innen anzeigen</label>
</div>
<div class="max-h-48 overflow-y-auto border border-slate-300 rounded bg-white text-sm">
<div class="flex-1 min-h-0 overflow-y-auto border border-slate-300 rounded bg-white text-sm">
<!-- All Persons List -->
<div id="all-persons">
{{ range $person := .Persons }}
@@ -114,5 +114,40 @@
</div>
</person-jump-filter>
</div>
<div class="p-4 w-full max-w-md flex flex-col h-full">
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
<i class="ri-map-pin-line text-slate-600"></i>
Auswahl nach Ort
</h3>
<!-- Place Jump Filter -->
<place-jump-filter class="flex-1 flex flex-col min-h-0">
<div class="space-y-3 flex flex-col h-full">
<div class="flex items-center gap-2">
<label for="place-search" class="hidden text-sm text-slate-600 w-16">Filter</label>
<input
type="text"
id="place-search"
placeholder="Ortsname eingeben..."
class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
>
</div>
<div class="flex-1 min-h-0 overflow-y-auto border border-slate-300 rounded bg-white text-sm">
<!-- All Places List -->
<div id="all-places">
{{ range $place := .Places }}
<div class="place-item odd:bg-slate-50 even:bg-white">
<a href="/ort/{{ $place.ID }}" class="block px-2 py-1 hover:bg-blue-50 border-b border-slate-100 last:border-b-0">
<span class="place-name font-medium text-slate-800">{{ $place.Name }}</span>
</a>
</div>
{{ end }}
</div>
</div>
</div>
</place-jump-filter>
</div>
</div>

View File

@@ -6,7 +6,7 @@
<div class="mb-6">
<a href="/ort/" class="inline-flex items-center text-blue-600 hover:text-blue-700 text-sm">
<i class="ri-arrow-left-line mr-2"></i>
Zur<75>ck zur <20>bersicht
Zur<75>ck zur <20>bersicht
</a>
</div>
@@ -22,7 +22,9 @@
{{ if .model.SelectedPlace.Place.Geo }}
<p class="text-slate-600">
<i class="ri-map-pin-line mr-1"></i>
Geonames: {{ .model.SelectedPlace.Place.Geo }}
<a href="{{ .model.SelectedPlace.Place.Geo }}" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-700 underline">
Geonames
</a>
</p>
{{ end }}
</div>
@@ -30,38 +32,66 @@
<!-- Associated Pieces -->
<div>
<h2 class="text-xl font-semibold text-slate-800 mb-4">
Beitr<EFBFBD>ge ({{ len .model.SelectedPlace.Pieces }})
<i class="ri-newspaper-line mr-2"></i><u class="decoration underline-offset-3">Beiträge</u> ({{ len .model.SelectedPlace.Pieces }})
</h2>
{{ if .model.SelectedPlace.Pieces }}
<div class="space-y-4">
{{ range $piece := .model.SelectedPlace.Pieces }}
<div class="border border-slate-200 rounded-lg p-4 hover:bg-slate-50">
<h3 class="font-medium text-slate-800 mb-2">
{{ if $piece.Title }}
{{ index $piece.Title 0 }}
{{ else }}
Untitled
{{ end }}
</h3>
<div class="space-y-2">
{{- /* Group pieces by their own title/incipit */ -}}
{{- $groupedPieces := dict -}}
{{- range $_, $p := .model.SelectedPlace.Pieces -}}
{{- $groupKey := "" -}}
{{- if $p.Title -}}
{{- $groupKey = index $p.Title 0 -}}
{{- else if $p.Incipit -}}
{{- $groupKey = index $p.Incipit 0 -}}
{{- else -}}
{{- $groupKey = printf "untitled-%s" $p.ID -}}
{{- end -}}
{{ if $piece.IssueRefs }}
<div class="text-sm text-slate-600">
{{ range $issueRef := $piece.IssueRefs }}
<span class="inline-block mr-4">
<i class="ri-calendar-line mr-1"></i>
<a href="/{{ $issueRef.When.Year }}/{{ $issueRef.IssueNumber }}" class="text-blue-600 hover:text-blue-700">
{{ $issueRef.When.Year }} Nr. {{ $issueRef.IssueNumber }}
{{- $existing := index $groupedPieces $groupKey -}}
{{- if $existing -}}
{{- $groupedPieces = merge $groupedPieces (dict $groupKey (append $existing $p)) -}}
{{- else -}}
{{- $groupedPieces = merge $groupedPieces (dict $groupKey (slice $p)) -}}
{{- end -}}
{{- end -}}
<div class="columns-2 gap-1 hyphens-auto">
{{- /* Display grouped pieces */ -}}
{{- range $groupKey, $groupedItems := $groupedPieces -}}
<div class="break-inside-avoid pl-4">
<div class="pb-1 indent-4">
{{- /* Use first piece for display text with colon format for places */ -}}
{{ template "_piece_summary_for_place" (dict "Piece" (index $groupedItems 0) "CurrentActorID" "") }}
{{- /* Show all citations from all pieces in this group inline with commas */ -}}
{{ " " }}{{- range $groupIndex, $groupItem := $groupedItems -}}
{{- range $issueIndex, $issue := $groupItem.IssueRefs -}}
{{- if or (gt $groupIndex 0) (gt $issueIndex 0) }}, {{ end -}}
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{- template "_citation" $issue -}}</span>{{- end -}}
{{- end -}}
{{- /* Add "Ganzer Beitrag" link if piece spans multiple issues */ -}}
{{- $firstGroupItem := index $groupedItems 0 -}}
{{- if gt (len $firstGroupItem.IssueRefs) 1 -}}
{{ " " }}<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:border-blue-300 rounded text-xs font-medium transition-colors duration-200
indent-0">
<i class="ri-file-copy-2-line text-xs"></i>
<a href="{{ GetPieceURL $firstGroupItem.ID }}" class="">
Ganzer Beitrag
</a>
</span>
{{ end }}
</div>
{{- end }}
</div>
{{ end }}
</div>
{{ end }}
</div>
{{- end -}}
</div>
</div>
{{ else }}
<p class="text-slate-500 italic">Keine Beitr<EFBFBD>ge f<EFBFBD>r diesen Ort gefunden.</p>
<p class="text-slate-500 italic">Keine Beiträge für diesen Ort gefunden.</p>
{{ end }}
</div>
</div>

View File

@@ -122,6 +122,7 @@ if (document.querySelector(".newspaper-page-container")) {
let htmxAfterSwapHandler = function (event) {
// Apply page-specific backdrop styling after navigation
applyPageBackdrop();
// Update citation links after navigation
updateCitationLinks();

View File

@@ -1,64 +1,3 @@
const filterBtn = document.getElementById("filter-toggle");
if (filterBtn) {
filterBtn.addEventListener("click", toggleFilter);
}
// Filter toggle functionality
function toggleFilter() {
const filterContainer = document.getElementById("filter-container");
const filterButton = document.getElementById("filter-toggle");
const filterContentDiv = filterContainer?.querySelector("div.flex.justify-center");
if (filterContainer.classList.contains("hidden")) {
// Show the filter
filterContainer.classList.remove("hidden");
filterButton.classList.add("bg-slate-200");
// Load content only if it doesn't exist - check for actual content
const hasContent = filterContentDiv && filterContentDiv.querySelector("div, form, h3");
if (!hasContent) {
htmx
.ajax("GET", "/filter", {
target: "#filter-container",
select: "#filter",
swap: "innerHTML",
})
.then(() => {
console.log("HTMX request completed");
// Re-query the element to see if it changed
const updatedDiv = document.querySelector("#filter-container .flex.justify-center");
})
.catch((error) => {
console.log("HTMX request failed:", error);
});
}
} else {
filterContainer.classList.add("hidden");
filterButton.classList.remove("bg-slate-200");
}
}
// Export for global access
window.toggleFilter = toggleFilter;
// Close filter when clicking outside
document.addEventListener("click", function (event) {
const filterContainer = document.getElementById("filter-container");
const filterButton = document.getElementById("filter-toggle");
if (
filterContainer &&
filterButton &&
!filterContainer.contains(event.target) &&
!filterButton.contains(event.target)
) {
if (!filterContainer.classList.contains("hidden")) {
filterContainer.classList.add("hidden");
filterButton.classList.remove("bg-slate-200");
}
}
});
// Handle search input logic
document.body.addEventListener("htmx:configRequest", function (event) {
let element = event.detail.elt;
@@ -158,6 +97,60 @@ class PersonJumpFilter extends HTMLElement {
// Register the custom element
customElements.define('person-jump-filter', PersonJumpFilter);
/**
* PlaceJumpFilter - Web component for filtering places list
* Works with server-rendered place list and provides client-side filtering
*/
class PlaceJumpFilter extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.setupEventListeners();
}
setupEventListeners() {
const searchInput = this.querySelector('#place-search');
const allPlacesList = this.querySelector('#all-places');
if (!searchInput || !allPlacesList) {
return;
}
// Search functionality
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
this.filterPlaces(query);
});
}
filterPlaces(query) {
const allPlacesList = this.querySelector('#all-places');
if (!allPlacesList) {
return;
}
const placeItems = allPlacesList.querySelectorAll('.place-item');
placeItems.forEach(item => {
const name = item.querySelector('.place-name')?.textContent || '';
const matches = !query || name.toLowerCase().includes(query);
if (matches) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
}
// Register the custom element
customElements.define('place-jump-filter', PlaceJumpFilter);
/**
* YearJumpFilter - Unified web component for Jahr-based navigation
* Allows jumping by Jahr/Ausgabe or Jahr/Seite
@@ -408,3 +401,303 @@ class YearJumpFilter extends HTMLElement {
// Register the custom element
customElements.define('year-jump-filter', YearJumpFilter);
/**
* SchnellauswahlButton - Web component for the quick selection/filter button
* Encapsulates the toggle functionality and HTMX content loading
*/
class SchnellauswahlButton extends HTMLElement {
constructor() {
super();
this.isOpen = false;
}
connectedCallback() {
this.createButton();
this.setupEventListeners();
}
disconnectedCallback() {
// Clean up event listeners when component is removed
document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('quickfilter:selection', this.handleSelectionEvent);
}
createButton() {
this.innerHTML = `
<button
id="filter-toggle"
class="mr-2 text-lg border px-4 h-full hover:bg-slate-200 transition-colors cursor-pointer"
title="Schnellfilter öffnen/schließen">
<i class="ri-filter-2-line"></i> <div class="inline-block text-lg">Schnellauswahl</div>
</button>
`;
}
setupEventListeners() {
const button = this.querySelector('button');
if (button) {
button.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleFilter();
});
}
// Bind event handlers to maintain 'this' context
this.handleSelectionEvent = this.handleSelectionEvent.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
// Listen for person/place selection events
document.addEventListener('quickfilter:selection', this.handleSelectionEvent);
// Listen for outside clicks
document.addEventListener('click', this.handleOutsideClick);
}
toggleFilter() {
const filterContainer = document.getElementById("filter-container");
const filterButton = this.querySelector('button');
if (!filterContainer || !filterButton) {
return;
}
const filterContentDiv = filterContainer.querySelector("div.flex.justify-center");
if (filterContainer.classList.contains("hidden")) {
// Show the filter
filterContainer.classList.remove("hidden");
filterButton.classList.add("bg-slate-200");
this.isOpen = true;
// Load content only if it doesn't exist - check for actual content
const hasContent = filterContentDiv && filterContentDiv.querySelector("div, form, h3");
if (!hasContent) {
htmx
.ajax("GET", "/filter", {
target: "#filter-container",
select: "#filter",
swap: "innerHTML",
})
.then(() => {
console.log("HTMX request completed");
// Re-query the element to see if it changed
const updatedDiv = document.querySelector("#filter-container .flex.justify-center");
})
.catch((error) => {
console.log("HTMX request failed:", error);
});
}
} else {
this.hideFilter();
}
}
hideFilter() {
const filterContainer = document.getElementById("filter-container");
const filterButton = this.querySelector('button');
if (!filterContainer || !filterButton) {
return;
}
filterContainer.classList.add("hidden");
filterButton.classList.remove("bg-slate-200");
this.isOpen = false;
}
handleSelectionEvent(event) {
if (this.isOpen) {
this.hideFilter();
}
}
handleOutsideClick(event) {
const filterContainer = document.getElementById("filter-container");
const filterButton = this.querySelector('button');
if (
this.isOpen &&
filterContainer &&
filterButton &&
!filterContainer.contains(event.target) &&
!this.contains(event.target)
) {
this.hideFilter();
}
}
}
// Register the custom element
customElements.define('schnellauswahl-button', SchnellauswahlButton);
/**
* NavigationMenu - Web component for the main navigation menu
* Web component for the main navigation menu with proper event handling
*/
class NavigationMenu extends HTMLElement {
constructor() {
super();
this.isOpen = false;
}
connectedCallback() {
this.createMenu();
this.setupEventListeners();
}
disconnectedCallback() {
document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('quickfilter:selection', this.handleSelectionEvent);
}
createMenu() {
this.innerHTML = `
<div>
<button
class="ml-2 text-2xl border px-4 h-full hover:bg-slate-200 transition-colors cursor-pointer"
id="menu-toggle">
<i class="ri-menu-line"></i>
</button>
<div id="menu-dropdown" class="hidden absolute bg-slate-50 px-5 py-3 z-50">
<div>
<div>Übersicht nach</div>
<div class="ml-2 flex flex-col gap-y-2 mt-2">
<a href="/">Jahrgängen</a>
<a href="/akteure/a">Personen</a>
<a href="/kategorie/">Betragsarten</a>
<a href="/ort/">Orten</a>
</div>
</div>
<div class="flex flex-col gap-y-2 mt-2">
<a href="/edition/">Geschichte & Edition der KGPZ</a>
<a href="/zitation/">Zitation</a>
<a href="/kontakt/">Kontakt</a>
</div>
</div>
</div>
`;
}
setupEventListeners() {
const button = this.querySelector('#menu-toggle');
const dropdown = this.querySelector('#menu-dropdown');
if (button) {
button.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleMenu();
});
}
// Listen for link clicks within the menu
if (dropdown) {
dropdown.addEventListener('click', (e) => {
const link = e.target.closest('a[href]');
if (link) {
// Dispatch selection event
const selectionEvent = new CustomEvent('quickfilter:selection', {
detail: {
type: 'navigation',
source: 'menu',
url: link.getAttribute('href'),
text: link.textContent.trim()
},
bubbles: true
});
document.dispatchEvent(selectionEvent);
}
});
}
// Bind event handlers for cleanup
this.handleOutsideClick = this.handleOutsideClick.bind(this);
this.handleSelectionEvent = this.handleSelectionEvent.bind(this);
// Listen for outside clicks
document.addEventListener('click', this.handleOutsideClick);
// Listen for selection events to close menu
document.addEventListener('quickfilter:selection', this.handleSelectionEvent);
}
toggleMenu() {
const button = this.querySelector('#menu-toggle');
const dropdown = this.querySelector('#menu-dropdown');
if (!button || !dropdown) return;
if (this.isOpen) {
this.hideMenu();
} else {
this.showMenu();
}
}
showMenu() {
const button = this.querySelector('#menu-toggle');
const dropdown = this.querySelector('#menu-dropdown');
if (!button || !dropdown) return;
dropdown.classList.remove('hidden');
button.classList.add('bg-slate-200');
this.isOpen = true;
}
hideMenu() {
const button = this.querySelector('#menu-toggle');
const dropdown = this.querySelector('#menu-dropdown');
if (!button || !dropdown) return;
dropdown.classList.add('hidden');
button.classList.remove('bg-slate-200');
this.isOpen = false;
}
handleSelectionEvent(event) {
// Close menu when any selection is made (from quickfilter or menu)
if (this.isOpen) {
this.hideMenu();
}
}
handleOutsideClick(event) {
if (this.isOpen && !this.contains(event.target)) {
this.hideMenu();
}
}
}
// Register the custom element
customElements.define('navigation-menu', NavigationMenu);
/**
* Global event handler for quickfilter selections
* Dispatches custom events when users select persons or places from quickfilter
*/
document.addEventListener('DOMContentLoaded', function() {
// Add event delegation for person and place links in quickfilter
document.addEventListener('click', function(event) {
// Check if the clicked element is a person or place link within the quickfilter
const quickfilterTarget = event.target.closest('a[href^="/akteure/"], a[href^="/ort/"]');
const filterContainer = document.getElementById('filter-container');
if (quickfilterTarget && filterContainer && filterContainer.contains(quickfilterTarget)) {
// Dispatch custom event to notify components
const selectionEvent = new CustomEvent('quickfilter:selection', {
detail: {
type: quickfilterTarget.getAttribute('href').startsWith('/akteure/') ? 'person' : 'place',
source: 'quickfilter',
id: quickfilterTarget.getAttribute('href').split('/').pop(),
url: quickfilterTarget.getAttribute('href')
},
bubbles: true
});
document.dispatchEvent(selectionEvent);
}
});
});