From ff333660be48c5511dbfe7ff72d4d52abf0b7b8f Mon Sep 17 00:00:00 2001 From: Simon Martens Date: Mon, 29 Sep 2025 22:13:32 +0200 Subject: [PATCH] Fixed some search things --- helpers/xsdtime/xsdtime.go | 2 +- views/assets/scripts.js | 216 ++++++++++++------ views/assets/style.css | 2 +- views/layouts/components/_menu.gohtml | 13 +- .../ausgabe/components/_title_nav.gohtml | 2 +- views/routes/components/_akteur_werke.gohtml | 49 +++- views/routes/search/body.gohtml | 8 +- views/transform/main.js | 19 ++ views/transform/search.js | 151 ++++++++++++ 9 files changed, 372 insertions(+), 90 deletions(-) diff --git a/helpers/xsdtime/xsdtime.go b/helpers/xsdtime/xsdtime.go index 3a35f8f..42cf38d 100644 --- a/helpers/xsdtime/xsdtime.go +++ b/helpers/xsdtime/xsdtime.go @@ -42,7 +42,7 @@ const ( ) var ( - MonthNameShort = []string{"Jan", "Feb", "März", "Apr", "Mai", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"} + MonthNameShort = []string{"Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez."} MonthName = []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} ) diff --git a/views/assets/scripts.js b/views/assets/scripts.js index ef316ae..d63d1c7 100644 --- a/views/assets/scripts.js +++ b/views/assets/scripts.js @@ -2,7 +2,7 @@ document.body.addEventListener("htmx:configRequest", function(a) { let e = a.detail.elt; e.id === "search" && e.value === "" && (a.detail.parameters = {}, a.detail.path = window.location.pathname + window.location.search); }); -class R extends HTMLElement { +class O extends HTMLElement { constructor() { super(); } @@ -35,7 +35,7 @@ class R extends HTMLElement { }); } } -customElements.define("person-jump-filter", R); +customElements.define("person-jump-filter", O); class V extends HTMLElement { connectedCallback() { const e = this.querySelector("#place-search"); @@ -376,6 +376,78 @@ document.addEventListener("DOMContentLoaded", function() { } }); }); +class _ extends HTMLElement { + constructor() { + super(), this.htmxRequestListeners = [], this.htmxAfterSwapListener = null; + } + connectedCallback() { + this.createSearchBar(), this.setupEventListeners(); + } + disconnectedCallback() { + this.cleanup(); + } + createSearchBar() { + this.innerHTML = ` +
+ +
+ +
+ +
+ `; + } + setupEventListeners() { + const e = this.querySelector("#search"), t = this.querySelector("#search-reset"), i = this.querySelector("#search-loading"); + if (!e || !t) return; + this.currentURL = window.location.pathname; + const n = () => { + e.value.trim() !== "" ? t.classList.remove("hidden") : t.classList.add("hidden"); + }; + if (n(), e.addEventListener("input", n), t.addEventListener("click", () => { + e.value = "", t.classList.add("hidden"), e.dispatchEvent(new Event("input", { bubbles: !0 })), e.focus(); + }), i) { + const s = (r) => { + r.detail.elt === e && (t.style.display = "none"); + }, o = (r) => { + r.detail.elt === e && (t.style.display = "", n()); + }; + document.body.addEventListener("htmx:beforeRequest", s), document.body.addEventListener("htmx:afterRequest", o), this.htmxRequestListeners.push( + { event: "htmx:beforeRequest", handler: s }, + { event: "htmx:afterRequest", handler: o } + ); + } + this.linkClickHandler = (s) => { + const r = s.target.closest("a[href]"); + r && r.getAttribute("href") && e.value.trim() !== "" && (e.value = "", t.classList.add("hidden")); + }, document.addEventListener("click", this.linkClickHandler); + } + cleanup() { + this.htmxRequestListeners.forEach(({ event: e, handler: t }) => { + document.body.removeEventListener(e, t); + }), this.htmxRequestListeners = [], this.linkClickHandler && (document.removeEventListener("click", this.linkClickHandler), this.linkClickHandler = null); + } +} +customElements.define("search-bar", _); const T = []; document.addEventListener("DOMContentLoaded", () => { I(); @@ -390,7 +462,7 @@ const I = function() { } } }; -class _ extends HTMLElement { +class W extends HTMLElement { constructor() { super(), this.scrollTimeout = null, this.clickHandlers = [], this.manualNavigation = !1, this.handleScroll = this.handleScroll.bind(this); } @@ -552,8 +624,8 @@ class _ extends HTMLElement { e && (e.style.opacity = "0", e.style.height = "0"), this.sections = null, this.navLinks = null, this.clickHandlers = [], this.manualNavigation = !1; } } -customElements.define("akteure-scrollspy", _); -class W extends HTMLElement { +customElements.define("akteure-scrollspy", W); +class K extends HTMLElement { constructor() { super(), this.searchInput = null, this.placeCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0; } @@ -605,7 +677,7 @@ class W extends HTMLElement { this.countElement && (t === "" ? this.countElement.textContent = `Alle Orte (${this.originalCount})` : e === 0 ? this.countElement.textContent = `Keine Orte gefunden für "${t}"` : this.countElement.textContent = `${e} von ${this.originalCount} Orten`); } } -class K extends HTMLElement { +class G extends HTMLElement { constructor() { super(), this.isExpanded = !1, this.isLoading = !1, this.hasLoaded = !1, this.boundHandleClick = this.handleClick.bind(this), this.boundHandleMapClick = this.handleMapClick.bind(this), this.boundHandleHeadingHover = this.handleHeadingHover.bind(this), this.boundHandleHeadingLeave = this.handleHeadingLeave.bind(this); } @@ -694,7 +766,7 @@ class K extends HTMLElement { e.addEventListener("htmx:afterRequest", t), e.addEventListener("htmx:responseError", i), htmx.trigger(e, "load-content"); } } -class G extends HTMLElement { +class Y extends HTMLElement { constructor() { super(), this.places = [], this.mapElement = null, this.pointsContainer = null, this.intersectionObserver = null, this.mapPoints = /* @__PURE__ */ new Map(), this.tooltip = null, this.showTimeout = null, this.hideTimeout = null, this.isTooltipVisible = !1, this.currentHoveredPlaceId = "", this.boundHandleHeadingHoverEvent = this.handleHeadingHoverEvent.bind(this); } @@ -743,8 +815,8 @@ class G extends HTMLElement { const o = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, r = { lon: 10, lat: 52 }, l = (u, d) => { const m = r.lon * Math.PI / 180, f = r.lat * Math.PI / 180, w = d * Math.PI / 180, b = u * Math.PI / 180, x = Math.sqrt( 2 / (1 + Math.sin(f) * Math.sin(b) + Math.cos(f) * Math.cos(b) * Math.cos(w - m)) - ), E = 6371e3 * x * Math.cos(b) * Math.sin(w - m), S = 6371e3 * x * (Math.cos(f) * Math.sin(b) - Math.sin(f) * Math.cos(b) * Math.cos(w - m)), L = E + 4321e3, P = S + 321e4, C = o.xmax - o.xmin, v = o.ymax - o.ymin, $ = (L - o.xmin) / C * 100, O = (o.ymax - P) / v * 100; - return { x: $, y: O }; + ), E = 6371e3 * x * Math.cos(b) * Math.sin(w - m), S = 6371e3 * x * (Math.cos(f) * Math.sin(b) - Math.sin(f) * Math.cos(b) * Math.cos(w - m)), C = E + 4321e3, k = S + 321e4, L = o.xmax - o.xmin, v = o.ymax - o.ymin, R = (C - o.xmin) / L * 100, $ = (o.ymax - k) / v * 100; + return { x: R, y: $ }; }, c = []; this.places.forEach((u) => { if (u.lat && u.lng) { @@ -778,8 +850,8 @@ class G extends HTMLElement { const v = f * w; x.x = d - (v - m) / 2, x.width = v; } - const E = 100 / x.width, S = -x.x, L = -x.y, P = `scale(${E}) translate(${S}%, ${L}%)`, C = this.querySelector(".transform-wrapper"); - C && (C.style.transform = P); + const E = 100 / x.width, S = -x.x, C = -x.y, k = `scale(${E}) translate(${S}%, ${C}%)`, L = this.querySelector(".transform-wrapper"); + L && (L.style.transform = k); } initializeScrollspy() { const e = document.querySelectorAll("place-accordion[data-place-id]"); @@ -881,7 +953,7 @@ class G extends HTMLElement { this.intersectionObserver && (this.intersectionObserver.disconnect(), this.intersectionObserver = null), this.clearTimeouts(), document.removeEventListener("place-heading-hover", this.boundHandleHeadingHoverEvent), window.removeEventListener("scroll", this.boundHandleScroll), document.removeEventListener("scroll", this.boundHandleScroll); } } -class Y extends HTMLElement { +class Z extends HTMLElement { constructor() { super(), this.place = null, this.mapElement = null, this.pointsContainer = null, this.tooltip = null; } @@ -920,8 +992,8 @@ class Y extends HTMLElement { const e = { xmin: 2555e3, ymin: 135e4, xmax: 7405e3, ymax: 55e5 }, t = { lon: 10, lat: 52 }, i = (r, l) => { const h = t.lon * Math.PI / 180, g = t.lat * Math.PI / 180, p = l * Math.PI / 180, m = r * Math.PI / 180, f = Math.sqrt( 2 / (1 + Math.sin(g) * Math.sin(m) + Math.cos(g) * Math.cos(m) * Math.cos(p - h)) - ), w = 6371e3 * f * Math.cos(m) * Math.sin(p - h), b = 6371e3 * f * (Math.cos(g) * Math.sin(m) - Math.sin(g) * Math.cos(m) * Math.cos(p - h)), x = w + 4321e3, E = b + 321e4, S = e.xmax - e.xmin, L = e.ymax - e.ymin, P = (x - e.xmin) / S * 100, C = (e.ymax - E) / L * 100; - return { x: P, y: C }; + ), w = 6371e3 * f * Math.cos(m) * Math.sin(p - h), b = 6371e3 * f * (Math.cos(g) * Math.sin(m) - Math.sin(g) * Math.cos(m) * Math.cos(p - h)), x = w + 4321e3, E = b + 321e4, S = e.xmax - e.xmin, C = e.ymax - e.ymin, k = (x - e.xmin) / S * 100, L = (e.ymax - E) / C * 100; + return { x: k, y: L }; }, n = parseFloat(this.place.lat), s = parseFloat(this.place.lng), o = i(n, s); if (o.x >= 0 && o.x <= 100 && o.y >= 0 && o.y <= 100) { const r = document.createElementNS("http://www.w3.org/2000/svg", "svg"); @@ -975,11 +1047,11 @@ class Y extends HTMLElement { this.tooltip.style.left = `${i}px`, this.tooltip.style.top = `${n}px`; } } -customElements.define("places-filter", W); -customElements.define("place-accordion", K); -customElements.define("places-map", G); -customElements.define("places-map-single", Y); -class Z extends HTMLElement { +customElements.define("places-filter", K); +customElements.define("place-accordion", G); +customElements.define("places-map", Y); +customElements.define("places-map-single", Z); +class X extends HTMLElement { constructor() { super(), this.searchInput = null, this.itemCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0; } @@ -1042,8 +1114,8 @@ class Z extends HTMLElement { this.countElement && (t === "" ? this.countElement.style.display = "none" : (this.countElement.style.display = "", e === 0 ? this.countElement.textContent = "(0)" : this.countElement.textContent = `(${e})`)); } } -customElements.define("generic-filter", Z); -class X extends HTMLElement { +customElements.define("generic-filter", X); +class J extends HTMLElement { constructor() { super(), this.resizeObserver = null; } @@ -1396,7 +1468,7 @@ class X extends HTMLElement { return "KGPZ"; } } -customElements.define("single-page-viewer", X); +customElements.define("single-page-viewer", J); document.body.addEventListener("htmx:beforeRequest", function(a) { const e = document.querySelector("single-page-viewer"); e && e.style.display !== "none" && (console.log("Cleaning up single page viewer before HTMX navigation"), e.close()); @@ -1405,7 +1477,7 @@ window.addEventListener("beforeunload", function() { const a = document.querySelector("single-page-viewer"); a && a.close(); }); -class J extends HTMLElement { +class U extends HTMLElement { constructor() { super(), this.isVisible = !1, this.scrollHandler = null, this.htmxAfterSwapHandler = null; } @@ -1446,8 +1518,8 @@ class J extends HTMLElement { }); } } -customElements.define("scroll-to-top-button", J); -class U extends HTMLElement { +customElements.define("scroll-to-top-button", U); +class Q extends HTMLElement { constructor() { super(), this.pageObserver = null, this.pageContainers = /* @__PURE__ */ new Map(), this.singlePageViewerActive = !1, this.singlePageViewerCurrentPage = null, this.boundHandleSinglePageViewer = this.handleSinglePageViewer.bind(this), this.eventListenersAttached = !1; } @@ -1578,8 +1650,8 @@ class U extends HTMLElement { this.pageObserver && (this.pageObserver.disconnect(), this.pageObserver = null), this.eventListenersAttached && (document.removeEventListener("singlepageviewer:opened", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:closed", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:pagechanged", this.boundHandleSinglePageViewer), this.eventListenersAttached = !1), this.pageContainers.clear(); } } -customElements.define("inhaltsverzeichnis-scrollspy", U); -class Q extends HTMLElement { +customElements.define("inhaltsverzeichnis-scrollspy", Q); +class ee extends HTMLElement { constructor() { super(), this.innerHTML = ` diff --git a/views/routes/components/_akteur_werke.gohtml b/views/routes/components/_akteur_werke.gohtml index 66aedf1..cfa0dc8 100644 --- a/views/routes/components/_akteur_werke.gohtml +++ b/views/routes/components/_akteur_werke.gohtml @@ -21,7 +21,52 @@
{{- if ne (len $w.Item.Citation.InnerXML ) 0 -}}
- {{- /* Check person's role in this work */ -}} + {{- /* Get other associated persons for this work */ -}} + {{- $otherAuthors := slice -}} + {{- $otherTranslators := slice -}} + {{- $otherEditors := slice -}} + {{- range $workAgentRef := $w.Item.AgentRefs -}} + {{- if ne $workAgentRef.Ref $a.ID -}} + {{- if or (eq $workAgentRef.Category "") (eq $workAgentRef.Category "autor") -}} + {{- $otherAuthors = append $otherAuthors $workAgentRef.Ref -}} + {{- else if eq $workAgentRef.Category "übersetzer" -}} + {{- $otherTranslators = append $otherTranslators $workAgentRef.Ref -}} + {{- else if eq $workAgentRef.Category "herausgeber" -}} + {{- $otherEditors = append $otherEditors $workAgentRef.Ref -}} + {{- end -}} + {{- end -}} + {{- end -}} + + {{- /* Display other associated persons before the work citation */ -}} + {{- $hasAssociatedPersons := or (gt (len $otherAuthors) 0) (gt (len $otherTranslators) 0) (gt (len $otherEditors) 0) -}} + {{- if $hasAssociatedPersons -}} + {{ "mit " }} + {{- range $i, $authorID := $otherAuthors -}} + {{- if gt $i 0 }}, {{ end }} + {{- $agent := GetAgent $authorID -}} + {{- if and $agent (gt (len $agent.Names) 0) -}} + {{ index $agent.Names 0 }} + {{- end -}} + {{- end -}} + {{- /* Display translators with (Übers.) */ -}} + {{- range $i, $translatorID := $otherTranslators -}} + {{- if or (gt (len $otherAuthors) 0) (gt $i 0) }}, {{ end }} + {{- $agent := GetAgent $translatorID -}} + {{- if and $agent (gt (len $agent.Names) 0) -}} + {{ index $agent.Names 0 }} (Übers.) + {{- end -}} + {{- end -}} + {{- /* Display editors with (Hrsg.) */ -}} + {{- range $i, $editorID := $otherEditors -}} + {{- if or (gt (len $otherAuthors) 0) (gt (len $otherTranslators) 0) (gt $i 0) }}, {{ end }} + {{- $agent := GetAgent $editorID -}} + {{- if and $agent (gt (len $agent.Names) 0) -}} + {{ index $agent.Names 0 }} (Hrsg.) + {{- end -}} + {{- end -}} + : {{ end -}} + + {{- /* Check current person's role in this work for display after citation */ -}} {{- $personRole := "" -}} {{- range $workAgentRef := $w.Item.AgentRefs -}} {{- if eq $workAgentRef.Ref $a.ID -}} @@ -216,4 +261,4 @@
-{{ end }} \ No newline at end of file +{{ end }} diff --git a/views/routes/search/body.gohtml b/views/routes/search/body.gohtml index acffc37..170192d 100644 --- a/views/routes/search/body.gohtml +++ b/views/routes/search/body.gohtml @@ -7,7 +7,7 @@

- Personen + Personen {{ if $model.Agents.Items }}{{ len $model.Agents.Items }}{{ end }}

{{ if $model.Agents.Items }} @@ -47,7 +47,7 @@

- Orte + Orte {{ if $model.Places.Items }}{{ len $model.Places.Items }}{{ end }}

{{ if $model.Places.Items }} @@ -69,7 +69,7 @@

- Kategorien + Kategorien {{ if $model.Categories.Items }}{{ len $model.Categories.Items }}{{ end }}

{{ if $model.Categories.Items }} @@ -91,7 +91,7 @@

- Ausgaben + Ausgaben {{ if $model.Issues.Items }}{{ len $model.Issues.Items }}{{ end }}

{{ if $model.Issues.Items }} diff --git a/views/transform/main.js b/views/transform/main.js index 02c8751..8d430c3 100644 --- a/views/transform/main.js +++ b/views/transform/main.js @@ -21,9 +21,27 @@ import { initializeNewspaperLayout, } from "./issue.js"; + // Update citation links to highlight current page references function updateCitationLinks() { const currentPath = window.location.pathname; + + // Don't disable citation links on search pages - they should all remain clickable + // Check both URL path and if search results are currently displayed in DOM + const hasSearchResults = document.querySelector('main .grid.grid-cols-1.lg\\:grid-cols-3') !== null; // Search results layout + const hasSearchHeaders = document.querySelector('main h3 u') !== null; // Underlined section headers (Werke, Beiträge, etc.) + const isSearchPage = currentPath.includes('/search') || currentPath.includes('/suche') || hasSearchResults || hasSearchHeaders; + + if (isSearchPage) { + const citationLinks = document.querySelectorAll(".citation-link[data-citation-url]"); + citationLinks.forEach((link) => { + // Reset to default styling on search pages + link.classList.remove("text-red-700", "pointer-events-none"); + link.removeAttribute("aria-current"); + }); + return; + } + const citationLinks = document.querySelectorAll(".citation-link[data-citation-url]"); citationLinks.forEach((link) => { @@ -128,6 +146,7 @@ let htmxAfterSwapHandler = function (event) { // Update citation links after navigation updateCitationLinks(); + // Execute all queued functions ExecuteSettleQueue(); diff --git a/views/transform/search.js b/views/transform/search.js index 46d235d..15f688b 100644 --- a/views/transform/search.js +++ b/views/transform/search.js @@ -759,3 +759,154 @@ document.addEventListener("DOMContentLoaded", function () { } }); }); + +/** + * SearchBar - Web component for the main search functionality + * Encapsulates search input, loading indicator, reset button, and all related behavior + */ +class SearchBar extends HTMLElement { + constructor() { + super(); + this.htmxRequestListeners = []; + this.htmxAfterSwapListener = null; + } + + connectedCallback() { + this.createSearchBar(); + this.setupEventListeners(); + } + + disconnectedCallback() { + // Clean up all event listeners + this.cleanup(); + } + + createSearchBar() { + this.innerHTML = ` +
+ +
+ +
+ +
+ `; + } + + setupEventListeners() { + const searchInput = this.querySelector('#search'); + const resetButton = this.querySelector('#search-reset'); + const loadingIndicator = this.querySelector('#search-loading'); + + if (!searchInput || !resetButton) return; + + // Store the current URL to detect changes + this.currentURL = window.location.pathname; + + // Function to toggle reset button visibility + const toggleResetButton = () => { + if (searchInput.value.trim() !== '') { + resetButton.classList.remove('hidden'); + } else { + resetButton.classList.add('hidden'); + } + }; + + // Check initial state (in case of page refresh with existing search) + toggleResetButton(); + + // Show/hide reset button as user types + searchInput.addEventListener('input', toggleResetButton); + + // Handle reset button click + resetButton.addEventListener('click', () => { + searchInput.value = ''; + resetButton.classList.add('hidden'); + + // Trigger the same HTMX behavior that happens when manually clearing the field + // This will automatically navigate back to the previous page + searchInput.dispatchEvent(new Event('input', { bubbles: true })); + + // Focus back on search input + searchInput.focus(); + }); + + // Handle HTMX request lifecycle - hide reset button during loading + if (loadingIndicator) { + // Hide reset button when loading starts + const beforeRequestHandler = (event) => { + if (event.detail.elt === searchInput) { + resetButton.style.display = 'none'; + } + }; + + // Show reset button when loading ends (if there's still text) + const afterRequestHandler = (event) => { + if (event.detail.elt === searchInput) { + resetButton.style.display = ''; + toggleResetButton(); + } + }; + + document.body.addEventListener('htmx:beforeRequest', beforeRequestHandler); + document.body.addEventListener('htmx:afterRequest', afterRequestHandler); + + // Store references for cleanup + this.htmxRequestListeners.push( + { event: 'htmx:beforeRequest', handler: beforeRequestHandler }, + { event: 'htmx:afterRequest', handler: afterRequestHandler } + ); + } + + // Simple click listener to clear search when any link is clicked + this.linkClickHandler = (event) => { + const clickedElement = event.target; + const link = clickedElement.closest('a[href]'); + + // If a link was clicked and it has an href, clear the search + if (link && link.getAttribute('href') && searchInput.value.trim() !== '') { + searchInput.value = ''; + resetButton.classList.add('hidden'); + } + }; + + // Listen for clicks on the entire document + document.addEventListener('click', this.linkClickHandler); + } + + cleanup() { + // Remove HTMX request listeners + this.htmxRequestListeners.forEach(({ event, handler }) => { + document.body.removeEventListener(event, handler); + }); + this.htmxRequestListeners = []; + + // Remove link click listener + if (this.linkClickHandler) { + document.removeEventListener('click', this.linkClickHandler); + this.linkClickHandler = null; + } + } +} + +// Register the custom element +customElements.define("search-bar", SearchBar);