+
{{ template "_piece_inhaltsverzeichnis" . }}
-
+
{{ template "_piece_sequential_layout" . }}
diff --git a/views/routes/piece/head.gohtml b/views/routes/piece/head.gohtml
index 24fd1c3..99e1d42 100644
--- a/views/routes/piece/head.gohtml
+++ b/views/routes/piece/head.gohtml
@@ -1,32 +1 @@
-
KGPZ – {{ if .model.Title }}{{ .model.Title }}{{ else }}Beitrag{{ end }} ({{ len .model.IssueContexts }} Teil{{ if gt (len .model.IssueContexts) 1 }}e{{ end }})
-
-
-
-{{ if .targetPage }}
-
-{{ end }}
\ No newline at end of file
+
KGPZ – {{ if .model.Title }}{{ .model.Title }}{{ else }}Beitrag{{ end }} ({{ len .model.IssueContexts }} Teil{{ if gt (len .model.IssueContexts) 1 }}e{{ end }})
\ No newline at end of file
diff --git a/views/transform/akteure.js b/views/transform/akteure.js
new file mode 100644
index 0000000..a85a544
--- /dev/null
+++ b/views/transform/akteure.js
@@ -0,0 +1,405 @@
+// ===========================
+// AKTEURE/AUTHORS SCROLLSPY WEB COMPONENT
+// ===========================
+
+export class AkteureScrollspy extends HTMLElement {
+ constructor() {
+ super();
+ this.scrollHandler = null;
+ this.scrollTimeout = null;
+ this.clickHandlers = [];
+ this.manualNavigation = false;
+ }
+
+ connectedCallback() {
+ // Small delay to ensure DOM is fully rendered after HTMX swap
+ setTimeout(() => {
+ this.initializeScrollspyAfterDelay();
+ }, 100);
+ }
+
+ initializeScrollspyAfterDelay() {
+ // Find sections and nav links
+ this.sections = document.querySelectorAll(".author-section");
+ this.navLinks = document.querySelectorAll(".scrollspy-link");
+
+ if (this.sections.length === 0 || this.navLinks.length === 0) {
+ // Retry after a bit more time if elements not found
+ setTimeout(() => {
+ this.sections = document.querySelectorAll(".author-section");
+ this.navLinks = document.querySelectorAll(".scrollspy-link");
+ if (this.sections.length > 0 && this.navLinks.length > 0) {
+ this.initializeScrollspy();
+ }
+ }, 200);
+ return;
+ }
+
+ this.initializeScrollspy();
+ }
+
+ disconnectedCallback() {
+ this.cleanup();
+ }
+
+ initializeScrollspy() {
+ // Set up scroll handler
+ this.scrollHandler = () => {
+ clearTimeout(this.scrollTimeout);
+ this.scrollTimeout = setTimeout(() => {
+ this.updateActiveLink();
+ this.updateSidebarScrollToTopButton();
+ }, 50);
+ };
+
+ window.addEventListener("scroll", this.scrollHandler);
+
+ // Add smooth scroll on link click
+ this.navLinks.forEach((link) => {
+ const clickHandler = (e) => {
+ e.preventDefault();
+ const targetId = link.getAttribute("data-target");
+ const target = document.getElementById(targetId);
+ if (target) {
+ // Immediately update the active link highlighting on click
+ this.updateActiveLinkImmediate(targetId);
+
+ // Temporarily disable automatic sidebar scrolling during manual navigation
+ this.manualNavigation = true;
+
+ target.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+
+ // Re-enable automatic scrolling after navigation completes
+ setTimeout(() => {
+ this.manualNavigation = false;
+ // Ensure the full marker is visible after scroll settles
+ this.ensureMarkerVisibility();
+ }, 1000);
+ }
+ };
+
+ this.clickHandlers.push({ link, handler: clickHandler });
+ link.addEventListener("click", clickHandler);
+ });
+
+ // Initial active link update
+ this.updateActiveLink();
+
+ // Initial scroll-to-top button update
+ this.updateSidebarScrollToTopButton();
+ }
+
+ ensureMarkerVisibility() {
+ const slider = document.getElementById("scrollspy-slider");
+ const nav = document.getElementById("scrollspy-nav");
+
+ if (!slider || !nav || slider.style.opacity === "0") {
+ return;
+ }
+
+ const navRect = nav.getBoundingClientRect();
+ const sliderTop = parseFloat(slider.style.top);
+ const sliderHeight = parseFloat(slider.style.height);
+ const sliderBottom = sliderTop + sliderHeight;
+
+ // Check if the marker extends beyond the visible area
+ const navScrollTop = nav.scrollTop;
+ const navVisibleBottom = navScrollTop + navRect.height;
+
+ if (sliderBottom > navVisibleBottom) {
+ // Marker extends below visible area - scroll down to show the bottom
+ const scrollTarget = sliderBottom - navRect.height + 20; // 20px padding
+ nav.scrollTo({
+ top: scrollTarget,
+ behavior: "smooth",
+ });
+ } else if (sliderTop < navScrollTop) {
+ // Marker extends above visible area - scroll up to show the top
+ const scrollTarget = sliderTop - 20; // 20px padding
+ nav.scrollTo({
+ top: Math.max(0, scrollTarget),
+ behavior: "smooth",
+ });
+ }
+ }
+
+ updateActiveLink() {
+ // Safety check: ensure DOM elements still exist
+ if (!this.sections || !this.navLinks) {
+ return;
+ }
+
+ const visibleSections = [];
+ const viewportTop = window.scrollY;
+ const viewportBottom = viewportTop + window.innerHeight;
+
+ // Check which sections have any part of their Werke or Beiträge visible
+ try {
+ this.sections.forEach((section) => {
+ if (!section || !section.getAttribute) return;
+ const sectionId = section.getAttribute("id");
+
+ // Find Werke and Beiträge sections within this author section
+ const werkeSection = section.querySelector(".akteur-werke-section");
+ const beitraegeSection = section.querySelector(".akteur-beitraege-section");
+
+ let isVisible = false;
+
+ // Check if any part of Werke section is visible
+ if (werkeSection) {
+ const werkeRect = werkeSection.getBoundingClientRect();
+ const werkeTopVisible = werkeRect.top < window.innerHeight;
+ const werkeBottomVisible = werkeRect.bottom > 0;
+ if (werkeTopVisible && werkeBottomVisible) {
+ isVisible = true;
+ }
+ }
+
+ // Check if any part of Beiträge section is visible
+ if (beitraegeSection && !isVisible) {
+ const beitraegeRect = beitraegeSection.getBoundingClientRect();
+ const beitraegeTopVisible = beitraegeRect.top < window.innerHeight;
+ const beitraegeBottomVisible = beitraegeRect.bottom > 0;
+ if (beitraegeTopVisible && beitraegeBottomVisible) {
+ isVisible = true;
+ }
+ }
+
+ // Fallback: if no Werke/Beiträge sections, check header visibility (for authors without content)
+ if (!werkeSection && !beitraegeSection) {
+ const headerElement = section.querySelector("div:first-child");
+ if (headerElement) {
+ const headerRect = headerElement.getBoundingClientRect();
+ const headerTopVisible = headerRect.top >= 0;
+ const headerBottomVisible = headerRect.bottom <= window.innerHeight;
+ if (headerTopVisible && headerBottomVisible) {
+ isVisible = true;
+ }
+ }
+ }
+
+ if (isVisible) {
+ visibleSections.push(sectionId);
+ }
+ });
+ } catch (e) {
+ // Handle case where sections became stale during DOM manipulation
+ return;
+ }
+
+ // Update highlighting with sliding background animation
+ const activeLinks = [];
+ const slider = document.getElementById("scrollspy-slider");
+
+ // Reset all links to default state (no background, just font weight for active)
+ this.navLinks.forEach((link) => {
+ link.classList.remove("font-medium");
+
+ const targetId = link.getAttribute("data-target");
+ if (visibleSections.includes(targetId)) {
+ link.classList.add("font-medium");
+ activeLinks.push(link);
+ }
+ });
+
+ // Calculate and animate the sliding background
+ if (activeLinks.length > 0 && slider) {
+ // Get the nav container for relative positioning
+ const nav = document.getElementById("scrollspy-nav");
+ const navRect = nav.getBoundingClientRect();
+
+ // Calculate the combined area of all active links
+ let minTop = Infinity;
+ let maxBottom = -Infinity;
+
+ activeLinks.forEach((link) => {
+ const linkRect = link.getBoundingClientRect();
+ const relativeTop = linkRect.top - navRect.top + nav.scrollTop;
+ const relativeBottom = relativeTop + linkRect.height;
+
+ minTop = Math.min(minTop, relativeTop);
+ maxBottom = Math.max(maxBottom, relativeBottom);
+ });
+
+ // Handle gaps between non-consecutive active links
+ let totalHeight = maxBottom - minTop;
+
+ // Position and size the slider
+ slider.style.top = `${minTop}px`;
+ slider.style.height = `${totalHeight}px`;
+ slider.style.opacity = "1";
+
+ // Ensure the full marker is visible
+ setTimeout(() => this.ensureMarkerVisibility(), 100);
+ } else if (slider) {
+ // Hide slider when no active links
+ slider.style.opacity = "0";
+ }
+
+ // Implement proportional scrolling to keep active links visible
+ if (activeLinks.length > 0) {
+ this.updateSidebarScroll(activeLinks);
+ }
+ }
+
+ updateActiveLinkImmediate(targetId) {
+ // Safety check: ensure DOM elements still exist
+ if (!this.navLinks) return;
+
+ const slider = document.getElementById("scrollspy-slider");
+
+ // Reset all links
+ try {
+ this.navLinks.forEach((link) => {
+ if (link && link.classList) {
+ link.classList.remove("font-medium");
+ }
+ });
+ } catch (e) {
+ // Handle case where navLinks became stale
+ return;
+ }
+
+ // Highlight the clicked link
+ const clickedLink = document.querySelector(`[data-target="${targetId}"]`);
+ if (clickedLink) {
+ clickedLink.classList.add("font-medium");
+
+ // Update slider position for the single clicked link
+ if (slider) {
+ const nav = document.getElementById("scrollspy-nav");
+ if (nav) {
+ const navRect = nav.getBoundingClientRect();
+ const linkRect = clickedLink.getBoundingClientRect();
+
+ const relativeTop = linkRect.top - navRect.top + nav.scrollTop;
+ slider.style.top = `${relativeTop}px`;
+ slider.style.height = `${linkRect.height}px`;
+ slider.style.opacity = "1";
+ }
+ }
+ }
+ }
+
+ updateSidebarScroll(activeLinks) {
+ // Skip automatic scrolling during manual navigation
+ if (this.manualNavigation) return;
+
+ const sidebar = document.getElementById("scrollspy-nav");
+ if (!sidebar) return;
+
+ // Get the first active link as reference
+ const firstActiveLink = activeLinks[0];
+
+ // Calculate the main content scroll progress
+ const documentHeight = Math.max(
+ document.body.scrollHeight,
+ document.body.offsetHeight,
+ document.documentElement.clientHeight,
+ document.documentElement.scrollHeight,
+ document.documentElement.offsetHeight,
+ );
+ const viewportHeight = window.innerHeight;
+ const maxScroll = documentHeight - viewportHeight;
+ const scrollProgress = maxScroll > 0 ? window.scrollY / maxScroll : 0;
+
+ // Calculate sidebar scroll dimensions
+ const sidebarHeight = sidebar.clientHeight;
+ const sidebarScrollHeight = sidebar.scrollHeight;
+ const maxSidebarScroll = sidebarScrollHeight - sidebarHeight;
+
+ if (maxSidebarScroll > 0) {
+ // Calculate proportional scroll position
+ const targetSidebarScroll = scrollProgress * maxSidebarScroll;
+
+ // Get the position of the first active link within the sidebar
+ const linkRect = firstActiveLink.getBoundingClientRect();
+ const sidebarRect = sidebar.getBoundingClientRect();
+ const linkOffsetInSidebar = linkRect.top - sidebarRect.top + sidebar.scrollTop;
+
+ // Calculate the desired position (center the active link in the sidebar viewport)
+ const sidebarCenter = sidebarHeight / 2;
+ const centeredPosition = linkOffsetInSidebar - sidebarCenter;
+
+ // Use a blend of proportional scrolling and centering for smooth behavior
+ const blendFactor = 0.7; // 70% proportional, 30% centering
+ const finalScrollPosition =
+ blendFactor * targetSidebarScroll + (1 - blendFactor) * centeredPosition;
+
+ // Clamp to valid scroll range
+ const clampedPosition = Math.max(0, Math.min(maxSidebarScroll, finalScrollPosition));
+
+ // Apply smooth scrolling, but only if the difference is significant
+ const currentScrollTop = sidebar.scrollTop;
+ const scrollDifference = Math.abs(clampedPosition - currentScrollTop);
+
+ if (scrollDifference > 10) {
+ // Only scroll if more than 10px difference
+ sidebar.scrollTo({
+ top: clampedPosition,
+ behavior: "smooth",
+ });
+ }
+ }
+ }
+
+ updateSidebarScrollToTopButton() {
+ const button = document.getElementById("sidebar-scroll-to-top");
+ if (!button) return;
+
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ const viewportHeight = window.innerHeight;
+ const shouldShow = scrollTop > viewportHeight * 0.5; // Show after scrolling 50% of viewport
+
+ if (shouldShow) {
+ button.classList.remove("opacity-0");
+ button.classList.add("opacity-100");
+ } else {
+ button.classList.remove("opacity-100");
+ button.classList.add("opacity-0");
+ }
+ }
+
+ cleanup() {
+ // Remove scroll listener
+ if (this.scrollHandler) {
+ window.removeEventListener("scroll", this.scrollHandler);
+ this.scrollHandler = null;
+ }
+
+ // Clear timeout
+ if (this.scrollTimeout) {
+ clearTimeout(this.scrollTimeout);
+ this.scrollTimeout = null;
+ }
+
+ // Remove click handlers
+ if (this.clickHandlers && this.clickHandlers.length > 0) {
+ this.clickHandlers.forEach(({ link, handler }) => {
+ if (link && handler) {
+ link.removeEventListener("click", handler);
+ }
+ });
+ }
+
+ // Reset slider
+ const slider = document.getElementById("scrollspy-slider");
+ if (slider) {
+ slider.style.opacity = "0";
+ slider.style.height = "0";
+ }
+
+ // Clear all references to DOM elements and state
+ this.sections = null;
+ this.navLinks = null;
+ this.clickHandlers = [];
+ this.manualNavigation = false;
+ }
+}
+
+// Register the web component
+customElements.define("akteure-scrollspy", AkteureScrollspy);
+
diff --git a/views/transform/issue.js b/views/transform/issue.js
new file mode 100644
index 0000000..b5677d1
--- /dev/null
+++ b/views/transform/issue.js
@@ -0,0 +1,595 @@
+// ===========================
+// ISSUE/NEWSPAPER LAYOUT FUNCTIONS
+// ===========================
+
+// Global variables for state management
+window.currentPageContainers = window.currentPageContainers || [];
+window.currentActiveIndex = window.currentActiveIndex || 0;
+window.pageObserver = window.pageObserver || null;
+
+// Modal functions
+export function enlargePage(imgElement, pageNumber, isFromSpread, partNumber = null) {
+ // Get or create the single page viewer component
+ let viewer = document.querySelector("single-page-viewer");
+ if (!viewer) {
+ viewer = document.createElement("single-page-viewer");
+ document.body.appendChild(viewer);
+ }
+
+ // Determine if this is a beilage page
+ const isBeilage = imgElement.closest('[data-beilage="true"]') !== null;
+
+ // Get target page from template data if available
+ const targetPage =
+ window.templateData && window.templateData.targetPage ? window.templateData.targetPage : 0;
+
+ // Show the page in the viewer
+ viewer.show(imgElement.src, imgElement.alt, pageNumber, isBeilage, targetPage, partNumber);
+}
+
+export function closeModal() {
+ const modal = document.getElementById("pageModal");
+ modal.classList.add("hidden");
+}
+
+// Page navigation functions
+export function initializePageTracking() {
+ // Clean up existing observer
+ if (window.pageObserver) {
+ window.pageObserver.disconnect();
+ window.pageObserver = null;
+ }
+
+ // Reset state
+ window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container"));
+ window.currentActiveIndex = 0;
+ updateButtonStates();
+
+ // Set up new observer
+ const existingObserver = document.querySelector(".newspaper-page-container");
+ if (existingObserver) {
+ let visibleContainers = new Set();
+
+ window.pageObserver = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ const containerIndex = window.currentPageContainers.indexOf(entry.target);
+ if (containerIndex !== -1) {
+ if (entry.isIntersecting) {
+ visibleContainers.add(containerIndex);
+ } else {
+ visibleContainers.delete(containerIndex);
+ }
+ }
+ });
+
+ // Update currentActiveIndex to the first (topmost) visible container
+ if (visibleContainers.size > 0) {
+ const sortedVisible = Array.from(visibleContainers).sort((a, b) => a - b);
+ const newActiveIndex = sortedVisible[0];
+ if (newActiveIndex !== window.currentActiveIndex) {
+ window.currentActiveIndex = newActiveIndex;
+ updateButtonStates();
+ }
+ }
+ },
+ {
+ rootMargin: "-20% 0px -70% 0px",
+ },
+ );
+
+ window.currentPageContainers.forEach((container) => {
+ window.pageObserver.observe(container);
+ });
+ }
+}
+
+export function scrollToPreviousPage() {
+ if (window.currentActiveIndex > 0) {
+ // Find the first page that's not currently visible
+ let targetIndex = -1;
+
+ // Check which pages are currently visible
+ const visibleIndexes = [];
+ window.currentPageContainers.forEach((container, index) => {
+ const containerRect = container.getBoundingClientRect();
+ const viewportHeight = window.innerHeight;
+
+ const visibleTop = Math.max(containerRect.top, 0);
+ const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
+ const containerHeight = containerRect.height;
+
+ const visibilityRatio = visibleHeight / containerHeight;
+ if (visibilityRatio >= 0.3) {
+ // Consider visible if 30% or more is showing
+ visibleIndexes.push(index);
+ }
+ });
+
+ // Find the first non-visible page before the current visible range
+ const minVisibleIndex = Math.min(...visibleIndexes);
+ for (let i = minVisibleIndex - 1; i >= 0; i--) {
+ if (!visibleIndexes.includes(i)) {
+ targetIndex = i;
+ break;
+ }
+ }
+
+ // If no non-visible page found, go to the page just before the visible range
+ if (targetIndex === -1 && minVisibleIndex > 0) {
+ targetIndex = minVisibleIndex - 1;
+ }
+
+ if (targetIndex >= 0) {
+ window.currentActiveIndex = targetIndex;
+ window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+
+ // Update button states after a brief delay to let intersection observer catch up
+ setTimeout(() => {
+ updateButtonStates();
+ }, 100);
+ }
+ }
+}
+
+export function scrollToNextPage() {
+ if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
+ // Find the first page that's not currently visible
+ let targetIndex = -1;
+
+ // Check which pages are currently visible
+ const visibleIndexes = [];
+ window.currentPageContainers.forEach((container, index) => {
+ const containerRect = container.getBoundingClientRect();
+ const viewportHeight = window.innerHeight;
+
+ const visibleTop = Math.max(containerRect.top, 0);
+ const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
+ const containerHeight = containerRect.height;
+
+ const visibilityRatio = visibleHeight / containerHeight;
+ if (visibilityRatio >= 0.3) {
+ // Consider visible if 30% or more is showing
+ visibleIndexes.push(index);
+ }
+ });
+
+ // Find the first non-visible page after the current visible range
+ const maxVisibleIndex = Math.max(...visibleIndexes);
+ for (let i = maxVisibleIndex + 1; i < window.currentPageContainers.length; i++) {
+ if (!visibleIndexes.includes(i)) {
+ targetIndex = i;
+ break;
+ }
+ }
+
+ // If no non-visible page found, go to the page just after the visible range
+ if (targetIndex === -1 && maxVisibleIndex < window.currentPageContainers.length - 1) {
+ targetIndex = maxVisibleIndex + 1;
+ }
+
+ if (targetIndex >= 0 && targetIndex < window.currentPageContainers.length) {
+ window.currentActiveIndex = targetIndex;
+ window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+
+ // Update button states after a brief delay to let intersection observer catch up
+ setTimeout(() => {
+ updateButtonStates();
+ }, 100);
+ }
+ }
+}
+
+export function scrollToBeilage() {
+ // Check if we're currently viewing a Beilage section
+ const isViewingBeilage = isCurrentlyInBeilageSection();
+
+ if (isViewingBeilage) {
+ // Go back to main issue (first main page)
+ const firstMainPage = document.querySelector("#newspaper-content .newspaper-page-container");
+ if (firstMainPage) {
+ firstMainPage.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+ }
+ } else {
+ // Go to first beilage container
+ const beilageContainer = document.querySelector('[class*="border-t-2 border-amber-200"]');
+ if (beilageContainer) {
+ beilageContainer.scrollIntoView({
+ behavior: "smooth",
+ block: "start",
+ });
+ }
+ }
+}
+
+function isCurrentlyInBeilageSection() {
+ // Check which pages are currently visible
+ const visibleIndexes = [];
+ window.currentPageContainers.forEach((container, index) => {
+ const containerRect = container.getBoundingClientRect();
+ const viewportHeight = window.innerHeight;
+
+ const visibleTop = Math.max(containerRect.top, 0);
+ const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
+ const containerHeight = containerRect.height;
+
+ const visibilityRatio = visibleHeight / containerHeight;
+ if (visibilityRatio >= 0.3) {
+ // Consider visible if 30% or more is showing
+ visibleIndexes.push(index);
+ }
+ });
+
+ // Check if any visible page is a Beilage page
+ for (const index of visibleIndexes) {
+ const container = window.currentPageContainers[index];
+ if (container && container.id && container.id.includes("beilage-")) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export function updateButtonStates() {
+ const prevBtn = document.getElementById("prevPageBtn");
+ const nextBtn = document.getElementById("nextPageBtn");
+ const beilageBtn = document.getElementById("beilageBtn");
+
+ if (prevBtn) {
+ if (window.currentActiveIndex <= 0) {
+ prevBtn.style.display = "none";
+ } else {
+ prevBtn.style.display = "flex";
+ }
+ }
+
+ if (nextBtn) {
+ if (window.currentActiveIndex >= window.currentPageContainers.length - 1) {
+ nextBtn.style.display = "none";
+ } else {
+ nextBtn.style.display = "flex";
+ }
+ }
+
+ // Update Beilage button based on current location
+ if (beilageBtn) {
+ const isViewingBeilage = isCurrentlyInBeilageSection();
+ const icon = beilageBtn.querySelector("i");
+
+ if (isViewingBeilage) {
+ // Show "Go to Main Issue" state - use gray styling
+ beilageBtn.title = "Zur Hauptausgabe";
+ beilageBtn.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";
+ if (icon) {
+ icon.className = "ri-file-text-line text-lg lg:text-xl";
+ }
+ } else {
+ // Show "Go to Beilage" state - use amber styling
+ beilageBtn.title = "Zu Beilage";
+ beilageBtn.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";
+ if (icon) {
+ icon.className = "ri-attachment-line text-lg lg:text-xl";
+ }
+ }
+ }
+}
+
+// Share and citation functions
+export function shareCurrentPage() {
+ const button = document.getElementById("shareLinkBtn");
+
+ // Get current page information
+ let pageInfo = "";
+
+ // Try to get the currently visible page number from active containers
+ if (
+ window.currentActiveIndex !== undefined &&
+ window.currentPageContainers &&
+ window.currentPageContainers[window.currentActiveIndex]
+ ) {
+ const activeContainer = window.currentPageContainers[window.currentActiveIndex];
+ const pageElement = activeContainer.querySelector("[data-page]");
+ if (pageElement) {
+ const pageNumber = pageElement.getAttribute("data-page");
+ pageInfo = `/${pageNumber}`;
+ }
+ }
+
+ // Construct the shareable URL
+ const currentUrl = window.location.origin + window.location.pathname + pageInfo;
+
+ // Try to use Web Share API if available (mobile browsers)
+ if (navigator.share) {
+ navigator
+ .share({
+ title: document.title,
+ url: currentUrl,
+ })
+ .catch((err) => {
+ // Fallback to clipboard
+ copyToClipboard(currentUrl, button);
+ });
+ } else {
+ // Fallback: copy to clipboard
+ copyToClipboard(currentUrl, button);
+ }
+}
+
+function copyToClipboard(text, button) {
+ if (navigator.clipboard) {
+ navigator.clipboard
+ .writeText(text)
+ .then(() => {
+ showSimplePopup(button, "Link kopiert!");
+ })
+ .catch((err) => {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ });
+ } else {
+ // Fallback for older browsers
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ const successful = document.execCommand("copy");
+ showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
+ } catch (err) {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }
+}
+
+export function generateCitation() {
+ const button = document.getElementById("citationBtn");
+
+ // Get current page and issue information
+ const issueInfo = document.title || "KGPZ";
+
+ // Use clean URL without hash fragments
+ let cleanUrl = window.location.origin + window.location.pathname;
+
+ // Remove any hash fragments that might still exist
+ if (cleanUrl.includes("#")) {
+ cleanUrl = cleanUrl.split("#")[0];
+ }
+
+ // Basic citation format (can be expanded later)
+ const currentDate = new Date().toLocaleDateString("de-DE");
+ const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}. Digital verfügbar unter: ${cleanUrl} (Zugriff: ${currentDate}).`;
+
+ // Copy to clipboard
+ if (navigator.clipboard) {
+ navigator.clipboard
+ .writeText(citation)
+ .then(() => {
+ showSimplePopup(button, "Zitation kopiert!");
+ })
+ .catch((err) => {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ });
+ } else {
+ // Fallback for older browsers
+ const textarea = document.createElement("textarea");
+ textarea.value = citation;
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ const successful = document.execCommand("copy");
+ showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
+ } catch (err) {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }
+}
+
+function showSimplePopup(button, message) {
+ // Remove any existing popup
+ const existingPopup = document.querySelector(".simple-popup");
+ if (existingPopup) {
+ existingPopup.remove();
+ }
+
+ // Create popup element
+ const popup = document.createElement("div");
+ popup.className = "simple-popup";
+ popup.textContent = message;
+
+ // Style the popup
+ popup.style.cssText = `
+ position: fixed;
+ background: #374151;
+ color: white;
+ padding: 6px 12px;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ z-index: 1000;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ white-space: nowrap;
+ `;
+
+ // Position popup next to button
+ const buttonRect = button.getBoundingClientRect();
+ const viewportHeight = window.innerHeight;
+ const viewportWidth = window.innerWidth;
+
+ // Calculate position relative to viewport (since we're using fixed positioning)
+ let left = buttonRect.left - 10;
+ let top = buttonRect.bottom + 8;
+
+ // Ensure popup doesn't go off-screen
+ const popupWidth = 120; // Estimated popup width
+ const popupHeight = 32; // Estimated popup height
+
+ // Adjust horizontal position if too far right
+ if (left + popupWidth > viewportWidth) {
+ left = buttonRect.right - popupWidth + 10;
+ }
+
+ // Adjust vertical position if too far down (show above button instead)
+ if (top + popupHeight > viewportHeight) {
+ top = buttonRect.top - popupHeight - 8;
+ }
+
+ popup.style.left = Math.max(5, left) + "px";
+ popup.style.top = Math.max(5, top) + "px";
+
+ // Add to page
+ document.body.appendChild(popup);
+
+ // Fade in
+ setTimeout(() => {
+ popup.style.opacity = "1";
+ }, 10);
+
+ // Auto-remove after 2 seconds
+ setTimeout(() => {
+ popup.style.opacity = "0";
+ setTimeout(() => {
+ if (popup.parentNode) {
+ popup.remove();
+ }
+ }, 200);
+ }, 2000);
+}
+
+// Page-specific utilities
+export function copyPagePermalink(pageNumber, button, isBeilage = false) {
+ let pageUrl = "";
+ if (isBeilage) {
+ // For beilage pages, still use hash for now until beilage URLs are updated
+ pageUrl = window.location.origin + window.location.pathname + `#beilage-1-page-${pageNumber}`;
+ } else {
+ // For regular pages, use the new URL format
+ const pathParts = window.location.pathname.split("/");
+ if (pathParts.length >= 3) {
+ // Current URL format: /year/issue or /year/issue/page
+ // New format: /year/issue/page
+ const year = pathParts[1];
+ const issue = pathParts[2];
+ pageUrl = `${window.location.origin}/${year}/${issue}/${pageNumber}`;
+ } else {
+ // Fallback to current URL with page appended
+ pageUrl = window.location.origin + window.location.pathname + `/${pageNumber}`;
+ }
+ }
+
+ const currentUrl = pageUrl;
+
+ // Copy to clipboard
+ if (navigator.clipboard) {
+ navigator.clipboard
+ .writeText(currentUrl)
+ .then(() => {
+ showSimplePopup(button, "Link kopiert!");
+ })
+ .catch((err) => {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ });
+ } else {
+ // Fallback for older browsers
+ const textarea = document.createElement("textarea");
+ textarea.value = currentUrl;
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ const successful = document.execCommand("copy");
+ showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
+ } catch (err) {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }
+}
+
+export function generatePageCitation(pageNumber, button) {
+ // Get current issue information
+ const issueInfo = document.title || "KGPZ";
+
+ // Generate page URL in new format
+ const pathParts = window.location.pathname.split("/");
+ let pageUrl;
+ if (pathParts.length >= 3) {
+ const year = pathParts[1];
+ const issue = pathParts[2];
+ pageUrl = `${window.location.origin}/${year}/${issue}/${pageNumber}`;
+ } else {
+ pageUrl = `${window.location.origin}${window.location.pathname}/${pageNumber}`;
+ }
+
+ const currentUrl = pageUrl;
+
+ // Basic citation format for specific page
+ const currentDate = new Date().toLocaleDateString("de-DE");
+ const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}, Seite ${pageNumber}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
+
+ // Copy to clipboard
+ if (navigator.clipboard) {
+ navigator.clipboard
+ .writeText(citation)
+ .then(() => {
+ showSimplePopup(button, "Zitation kopiert!");
+ })
+ .catch((err) => {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ });
+ } else {
+ // Fallback for older browsers
+ const textarea = document.createElement("textarea");
+ textarea.value = citation;
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ const successful = document.execCommand("copy");
+ showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
+ } catch (err) {
+ showSimplePopup(button, "Kopieren fehlgeschlagen");
+ } finally {
+ document.body.removeChild(textarea);
+ }
+ }
+}
+
+// Initialize newspaper layout functionality
+export function initializeNewspaperLayout() {
+ // Initialize page tracking
+ initializePageTracking();
+
+ // Set up scroll handler
+ window.addEventListener("scroll", function () {
+ clearTimeout(window.scrollTimeout);
+ window.scrollTimeout = setTimeout(() => {
+ updateButtonStates(); // Update button states including Beilage toggle
+ }, 50);
+ });
+
+ // Set up keyboard shortcuts
+ document.addEventListener("keydown", function (e) {
+ if (e.key === "Escape") {
+ closeModal();
+ }
+ });
+}
\ No newline at end of file
diff --git a/views/transform/main.js b/views/transform/main.js
index e8f915e..445e568 100644
--- a/views/transform/main.js
+++ b/views/transform/main.js
@@ -1,1363 +1,95 @@
import "./site.css";
+import "./akteure.js";
+import { SinglePageViewer } from "./single-page-viewer.js";
+import { ScrollToTopButton } from "./scroll-to-top.js";
+import {
+ enlargePage,
+ closeModal,
+ scrollToPreviousPage,
+ scrollToNextPage,
+ scrollToBeilage,
+ shareCurrentPage,
+ generateCitation,
+ copyPagePermalink,
+ generatePageCitation,
+ initializeNewspaperLayout,
+} from "./issue.js";
-const ATTR_XSLT_ONLOAD = "script[xslt-onload]";
-const ATTR_XSLT_TEMPLATE = "xslt-template";
-const ATTR_XSLT_STATE = "xslt-transformed";
+// Update citation links to highlight current page references
+function updateCitationLinks() {
+ const currentPath = window.location.pathname;
+ const citationLinks = document.querySelectorAll(".citation-link[data-citation-url]");
-const xslt_processors = new Map();
+ citationLinks.forEach((link) => {
+ const citationUrl = link.getAttribute("data-citation-url");
+ let isCurrentPage = false;
-function setup_xslt() {
- let els = htmx.findAll(ATTR_XSLT_ONLOAD);
- for (let element of els) {
- transform_xslt(element);
- }
-}
-
-function transform_xslt(element) {
- if (
- element.getAttribute(ATTR_XSLT_STATE) === "true" ||
- !element.hasAttribute(ATTR_XSLT_TEMPLATE)
- ) {
- return;
- }
-
- let templateId = "#" + element.getAttribute(ATTR_XSLT_TEMPLATE);
- let processor = xslt_processors.get(templateId);
- if (!processor) {
- let template = htmx.find(templateId);
- if (template) {
- let content = template.innerHTML
- ? new DOMParser().parseFromString(template.innerHTML, "application/xml")
- : template.contentDocument;
- processor = new XSLTProcessor();
- processor.importStylesheet(content);
- xslt_processors.set(templateId, processor);
+ // Check for exact match
+ if (citationUrl === currentPath) {
+ isCurrentPage = true;
} else {
- throw new Error("Unknown XSLT template: " + templateId);
+ // Check if current path is an issue with page number that matches this citation
+ const currentPathMatch = currentPath.match(/^\/(\d{4})\/(\d+)(?:\/(\d+))?$/);
+ const citationUrlMatch = citationUrl.match(/^\/(\d{4})\/(\d+)$/);
+
+ if (currentPathMatch && citationUrlMatch) {
+ const [, currentYear, currentIssue, currentPage] = currentPathMatch;
+ const [, citationYear, citationIssue] = citationUrlMatch;
+
+ // If year and issue match, this citation refers to the current issue
+ if (currentYear === citationYear && currentIssue === citationIssue) {
+ isCurrentPage = true;
+ }
+ }
}
- }
- let data = new DOMParser().parseFromString(element.innerHTML, "application/xml");
- let frag = processor.transformToFragment(data, document);
- let s = new XMLSerializer().serializeToString(frag);
- element.outerHTML = s;
-}
-
-function setup_templates() {
- let templates = document.querySelectorAll("template[simple]");
- templates.forEach((template) => {
- let templateId = template.getAttribute("id");
- let templateContent = template.content;
-
- customElements.define(
- templateId,
- class extends HTMLElement {
- constructor() {
- super();
- this.appendChild(templateContent.cloneNode(true));
- this.slots = this.querySelectorAll("slot");
- }
-
- connectedCallback() {
- let toremove = [];
- this.slots.forEach((tslot) => {
- let slotName = tslot.getAttribute("name");
- let slotContent = this.querySelector(`[slot="${slotName}"]`);
- if (slotContent) {
- tslot.replaceWith(slotContent.cloneNode(true));
- toremove.push(slotContent);
- }
- });
- toremove.forEach((element) => {
- element.remove();
- });
- }
- },
- );
+ if (isCurrentPage) {
+ // Style as current page: red text, no underline, not clickable
+ link.classList.add("text-red-700", "pointer-events-none");
+ link.setAttribute("aria-current", "page");
+ } else {
+ // Reset to default styling for non-current pages
+ link.classList.remove("text-red-700", "pointer-events-none");
+ link.removeAttribute("aria-current");
+ }
});
}
-// ===========================
-// NEWSPAPER LAYOUT FUNCTIONS
-// ===========================
+// Apply page-specific backdrop styling based on URL
+function applyPageBackdrop() {
+ const path = window.location.pathname;
+ const body = document.body;
-// Global variables for state management
-window.highlightObserver = window.highlightObserver || null;
-window.currentPageContainers = window.currentPageContainers || [];
-window.currentActiveIndex = window.currentActiveIndex || 0;
-window.pageObserver = window.pageObserver || null;
-window.scrollTimeout = window.scrollTimeout || null;
-
-// Page highlighting functionality
-function initializePageHighlighting() {
- // Clean up existing observer
- if (window.highlightObserver) {
- window.highlightObserver.disconnect();
- window.highlightObserver = null;
- }
-
- // Get all page containers
- const pageContainers = document.querySelectorAll(".newspaper-page-container");
-
- // Set up intersection observer for active page tracking
- window.highlightObserver = new IntersectionObserver(
- (entries) => {
- checkAndHighlightVisiblePages();
- },
- {
- rootMargin: "-20% 0px -70% 0px",
- },
+ // Remove any existing page-specific classes
+ body.classList.remove(
+ "page-akteure",
+ "page-ausgabe",
+ "page-search",
+ "page-ort",
+ "page-kategorie",
+ "page-piece",
+ "page-edition",
);
- // Observe all page containers
- pageContainers.forEach((container) => {
- window.highlightObserver.observe(container);
- });
-}
-
-function checkAndHighlightVisiblePages() {
- const visiblePageNumbers = [];
- const allPageContainers = document.querySelectorAll(".newspaper-page-container");
-
- // Find visible page numbers
- allPageContainers.forEach((container) => {
- const containerRect = container.getBoundingClientRect();
- const viewportHeight = window.innerHeight;
-
- const visibleTop = Math.max(containerRect.top, 0);
- const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
- const visibleHeight = Math.max(0, visibleBottom - visibleTop);
- const containerHeight = containerRect.height;
-
- const visibilityRatio = visibleHeight / containerHeight;
- const isVisible = visibilityRatio >= 0.5;
-
- const pageImg = container.querySelector("img[data-page]");
- const pageNumber = pageImg ? pageImg.getAttribute("data-page") : "unknown";
-
- if (isVisible && pageImg && pageNumber && !visiblePageNumbers.includes(pageNumber)) {
- visiblePageNumbers.push(pageNumber);
- }
- });
-
- // Show continuations only for visible pages
- showContinuationsForVisiblePages(visiblePageNumbers);
-
- // Highlight visible pages
- if (visiblePageNumbers.length > 0) {
- markCurrentPagesInInhaltsverzeichnis(visiblePageNumbers);
+ // Determine page type from URL path and apply appropriate class
+ if (path.includes("/akteure/") || path.includes("/autoren")) {
+ body.classList.add("page-akteure");
+ } else if (path.match(/\/\d{4}\/\d+/)) {
+ // Issue URLs like /1771/42 or /1771/42/166
+ body.classList.add("page-ausgabe");
+ } else if (path.includes("/search") || path.includes("/suche")) {
+ body.classList.add("page-search");
+ } else if (path.includes("/ort/")) {
+ body.classList.add("page-ort");
+ } else if (path.includes("/kategorie/")) {
+ body.classList.add("page-kategorie");
+ } else if (path.includes("/beitrag/")) {
+ body.classList.add("page-piece");
+ } else if (path.includes("/edition")) {
+ body.classList.add("page-edition");
}
}
-function showContinuationsForVisiblePages(visiblePageNumbers) {
- // Hide all continuation entries by default
- document.querySelectorAll(".continuation-entry").forEach((entry) => {
- entry.style.display = "none";
- });
-
- // Show continuation entries only for visible pages
- visiblePageNumbers.forEach((pageNumber) => {
- const pageEntry = document.querySelector(`[data-page-container="${pageNumber}"]`);
- if (pageEntry) {
- const continuationEntries = pageEntry.querySelectorAll(".continuation-entry");
- continuationEntries.forEach((entry) => {
- entry.style.display = "";
- });
- }
- });
-
- // Update work titles based on highlighted state
- updateWorkTitles(visiblePageNumbers);
-
- // Update page entry visibility after showing/hiding continuations
- updatePageEntryVisibility();
-}
-
-function updateWorkTitles(visiblePageNumbers) {
- // Reset all work titles to short form
- document.querySelectorAll(".work-title").forEach((titleElement) => {
- const shortTitle = titleElement.getAttribute("data-short-title");
- if (shortTitle) {
- titleElement.textContent = shortTitle;
- }
- });
-
- // Update work titles to full form for highlighted pages
- visiblePageNumbers.forEach((pageNumber) => {
- const pageEntry = document.querySelector(`[data-page-container="${pageNumber}"]`);
- if (pageEntry) {
- const workTitles = pageEntry.querySelectorAll(".work-title");
- workTitles.forEach((titleElement) => {
- const fullTitle = titleElement.getAttribute("data-full-title");
- if (fullTitle && fullTitle !== titleElement.getAttribute("data-short-title")) {
- titleElement.textContent = fullTitle;
- }
- });
- }
- });
-}
-
-function updatePageEntryVisibility() {
- // Check each page entry to see if it has any visible content
- document.querySelectorAll(".page-entry").forEach((pageEntry) => {
- const allEntries = pageEntry.querySelectorAll(".inhalts-entry");
- let hasVisibleContent = false;
-
- // Check if any entry is visible
- allEntries.forEach((entry) => {
- const computedStyle = window.getComputedStyle(entry);
- if (computedStyle.display !== "none") {
- hasVisibleContent = true;
- }
- });
-
- // Hide the entire page entry if it has no visible content
- if (hasVisibleContent) {
- pageEntry.style.display = "";
- } else {
- pageEntry.style.display = "none";
- }
- });
-}
-
-function markCurrentPageInInhaltsverzeichnis(pageNumber) {
- markCurrentPagesInInhaltsverzeichnis([pageNumber]);
-}
-
-function markCurrentPagesInInhaltsverzeichnis(pageNumbers) {
- console.log("markCurrentPagesInInhaltsverzeichnis called with:", pageNumbers);
-
- // Reset all page container borders to default
- document.querySelectorAll("[data-page-container]").forEach((container) => {
- if (container.hasAttribute("data-beilage")) {
- container.classList.remove("border-red-500");
- container.classList.add("border-amber-400");
- } else {
- container.classList.remove("border-red-500");
- container.classList.add("border-slate-300");
- }
- });
-
- // Reset all page numbers in Inhaltsverzeichnis
- document.querySelectorAll(".page-number-inhalts").forEach((pageNum) => {
- pageNum.classList.remove("text-red-600", "font-bold");
- pageNum.classList.add("text-slate-700", "font-semibold");
- pageNum.style.textDecoration = "";
- pageNum.style.pointerEvents = "";
- // Restore hover effects
- if (pageNum.classList.contains("bg-blue-50")) {
- pageNum.classList.add("hover:bg-blue-100");
- } else if (pageNum.classList.contains("bg-amber-50")) {
- pageNum.classList.add("hover:bg-amber-100");
- }
- // Keep original background colors
- if (!pageNum.classList.contains("bg-amber-50") && !pageNum.classList.contains("bg-blue-50")) {
- pageNum.classList.add("bg-blue-50");
- }
- });
-
- // Reset all containers and links in Inhaltsverzeichnis
- document.querySelectorAll(".inhalts-entry").forEach((container) => {
- container.classList.add("hover:bg-slate-100");
- container.style.cursor = "";
- });
-
- document.querySelectorAll('.inhalts-entry a[href*="/"]').forEach((link) => {
- link.classList.remove("no-underline");
- if (link.classList.contains("bg-blue-50")) {
- link.classList.add("hover:bg-blue-100");
- }
- });
-
- // Find and highlight the current page numbers
- const highlightedElements = [];
-
- pageNumbers.forEach((pageNumber) => {
- // Find the exact page entry for this page number
- const pageNumElement = document.querySelector(
- `.page-number-inhalts[data-page-number="${pageNumber}"]`,
- );
-
- if (pageNumElement) {
- // Highlight the page number
- pageNumElement.classList.remove("text-slate-700", "hover:bg-blue-100", "hover:bg-amber-100");
- pageNumElement.classList.add("text-red-600", "font-bold");
- pageNumElement.style.textDecoration = "none";
- pageNumElement.style.pointerEvents = "none";
- highlightedElements.push(pageNumElement);
-
- // Highlight the page container's left border
- const pageContainer = document.querySelector(`[data-page-container="${pageNumber}"]`);
- if (pageContainer) {
- pageContainer.classList.remove("border-slate-300", "border-amber-400");
- pageContainer.classList.add("border-red-500");
- }
-
- // Make links in the current page non-clickable and remove hover effects
- const parentEntry = pageNumElement.closest(".page-entry");
- if (parentEntry) {
- // Remove hover effects from the container
- const entryContainers = parentEntry.querySelectorAll(".inhalts-entry");
- entryContainers.forEach((container) => {
- container.classList.remove("hover:bg-slate-100");
- container.style.cursor = "default";
- });
-
- // Also handle issue reference links
- parentEntry.querySelectorAll('a[href*="/"]').forEach((link) => {
- if (link.getAttribute("aria-current") === "page") {
- link.style.textDecoration = "none";
- link.style.pointerEvents = "none";
- link.classList.add("no-underline");
- link.classList.remove("hover:bg-blue-100");
- }
- });
- }
- }
- });
-
- // Auto-scroll to first highlighted element if it exists
- if (highlightedElements.length > 0) {
- scrollToHighlightedPage(highlightedElements[0]);
- }
-
- // Also highlight page indicators
- document.querySelectorAll(".page-indicator").forEach((indicator) => {
- indicator.classList.remove("text-red-600", "font-bold");
- indicator.classList.add("text-slate-600", "font-semibold");
- // Keep original backgrounds
- if (!indicator.classList.contains("bg-amber-50")) {
- indicator.classList.add("bg-blue-50");
- }
- });
-
- // Highlight page indicators for all current pages
- pageNumbers.forEach((pageNumber) => {
- const pageIndicator = document.querySelector(`.page-indicator[data-page="${pageNumber}"]`);
- if (pageIndicator) {
- pageIndicator.classList.remove("text-slate-600");
- pageIndicator.classList.add("text-red-600", "font-bold");
- }
- });
-}
-
-function scrollToHighlightedPage(element) {
- // Check if the element is in a scrollable container
- const inhaltsContainer = element.closest(".lg\\:overflow-y-auto");
- if (inhaltsContainer) {
- // Calculate position
- const containerRect = inhaltsContainer.getBoundingClientRect();
- const elementRect = element.getBoundingClientRect();
-
- // Check if element is not fully visible
- const isAboveContainer = elementRect.top < containerRect.top;
- const isBelowContainer = elementRect.bottom > containerRect.bottom;
-
- if (isAboveContainer || isBelowContainer) {
- // Scroll to make element visible with some padding
- element.scrollIntoView({
- behavior: "smooth",
- block: "center",
- });
- }
- }
-}
-
-// Modal functions
-function enlargePage(imgElement, pageNumber, isFromSpread, partNumber = null) {
- // Get or create the single page viewer component
- let viewer = document.querySelector("single-page-viewer");
- if (!viewer) {
- viewer = document.createElement("single-page-viewer");
- document.body.appendChild(viewer);
- }
-
- // Determine if this is a beilage page
- const isBeilage = imgElement.closest('[data-beilage="true"]') !== null;
-
- // Get target page from template data if available
- const targetPage =
- window.templateData && window.templateData.targetPage ? window.templateData.targetPage : 0;
-
- // Show the page in the viewer
- viewer.show(imgElement.src, imgElement.alt, pageNumber, isBeilage, targetPage, partNumber);
-}
-
-function closeModal() {
- const modal = document.getElementById("pageModal");
- modal.classList.add("hidden");
-}
-
-// Page navigation functions
-function initializePageTracking() {
- // Clean up existing observer
- if (window.pageObserver) {
- window.pageObserver.disconnect();
- window.pageObserver = null;
- }
-
- // Reset state
- window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container"));
- window.currentActiveIndex = 0;
- updateButtonStates();
-
- // Set up new observer
- const existingObserver = document.querySelector(".newspaper-page-container");
- if (existingObserver) {
- let visibleContainers = new Set();
-
- window.pageObserver = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- const containerIndex = window.currentPageContainers.indexOf(entry.target);
- if (containerIndex !== -1) {
- if (entry.isIntersecting) {
- visibleContainers.add(containerIndex);
- } else {
- visibleContainers.delete(containerIndex);
- }
- }
- });
-
- // Update currentActiveIndex to the first (topmost) visible container
- if (visibleContainers.size > 0) {
- const sortedVisible = Array.from(visibleContainers).sort((a, b) => a - b);
- const newActiveIndex = sortedVisible[0];
- if (newActiveIndex !== window.currentActiveIndex) {
- window.currentActiveIndex = newActiveIndex;
- updateButtonStates();
- }
- }
- },
- {
- rootMargin: "-20% 0px -70% 0px",
- },
- );
-
- window.currentPageContainers.forEach((container) => {
- window.pageObserver.observe(container);
- });
- }
-}
-
-function scrollToPreviousPage() {
- if (window.currentActiveIndex > 0) {
- // Find the first page that's not currently visible
- let targetIndex = -1;
-
- // Check which pages are currently visible
- const visibleIndexes = [];
- window.currentPageContainers.forEach((container, index) => {
- const containerRect = container.getBoundingClientRect();
- const viewportHeight = window.innerHeight;
-
- const visibleTop = Math.max(containerRect.top, 0);
- const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
- const visibleHeight = Math.max(0, visibleBottom - visibleTop);
- const containerHeight = containerRect.height;
-
- const visibilityRatio = visibleHeight / containerHeight;
- if (visibilityRatio >= 0.3) {
- // Consider visible if 30% or more is showing
- visibleIndexes.push(index);
- }
- });
-
- // Find the first non-visible page before the current visible range
- const minVisibleIndex = Math.min(...visibleIndexes);
- for (let i = minVisibleIndex - 1; i >= 0; i--) {
- if (!visibleIndexes.includes(i)) {
- targetIndex = i;
- break;
- }
- }
-
- // If no non-visible page found, go to the page just before the visible range
- if (targetIndex === -1 && minVisibleIndex > 0) {
- targetIndex = minVisibleIndex - 1;
- }
-
- if (targetIndex >= 0) {
- window.currentActiveIndex = targetIndex;
- window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
- behavior: "smooth",
- block: "start",
- });
-
- // Update button states after a brief delay to let intersection observer catch up
- setTimeout(() => {
- updateButtonStates();
- }, 100);
- }
- }
-}
-
-function scrollToNextPage() {
- if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
- // Find the first page that's not currently visible
- let targetIndex = -1;
-
- // Check which pages are currently visible
- const visibleIndexes = [];
- window.currentPageContainers.forEach((container, index) => {
- const containerRect = container.getBoundingClientRect();
- const viewportHeight = window.innerHeight;
-
- const visibleTop = Math.max(containerRect.top, 0);
- const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
- const visibleHeight = Math.max(0, visibleBottom - visibleTop);
- const containerHeight = containerRect.height;
-
- const visibilityRatio = visibleHeight / containerHeight;
- if (visibilityRatio >= 0.3) {
- // Consider visible if 30% or more is showing
- visibleIndexes.push(index);
- }
- });
-
- // Find the first non-visible page after the current visible range
- const maxVisibleIndex = Math.max(...visibleIndexes);
- for (let i = maxVisibleIndex + 1; i < window.currentPageContainers.length; i++) {
- if (!visibleIndexes.includes(i)) {
- targetIndex = i;
- break;
- }
- }
-
- // If no non-visible page found, go to the page just after the visible range
- if (targetIndex === -1 && maxVisibleIndex < window.currentPageContainers.length - 1) {
- targetIndex = maxVisibleIndex + 1;
- }
-
- if (targetIndex >= 0 && targetIndex < window.currentPageContainers.length) {
- window.currentActiveIndex = targetIndex;
- window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
- behavior: "smooth",
- block: "start",
- });
-
- // Update button states after a brief delay to let intersection observer catch up
- setTimeout(() => {
- updateButtonStates();
- }, 100);
- }
- }
-}
-
-function scrollToBeilage() {
- // Check if we're currently viewing a Beilage section
- const isViewingBeilage = isCurrentlyInBeilageSection();
-
- if (isViewingBeilage) {
- // Go back to main issue (first main page)
- const firstMainPage = document.querySelector("#newspaper-content .newspaper-page-container");
- if (firstMainPage) {
- firstMainPage.scrollIntoView({
- behavior: "smooth",
- block: "start",
- });
- }
- } else {
- // Go to first beilage container
- const beilageContainer = document.querySelector('[class*="border-t-2 border-amber-200"]');
- if (beilageContainer) {
- beilageContainer.scrollIntoView({
- behavior: "smooth",
- block: "start",
- });
- }
- }
-}
-
-function isCurrentlyInBeilageSection() {
- // Check which pages are currently visible
- const visibleIndexes = [];
- window.currentPageContainers.forEach((container, index) => {
- const containerRect = container.getBoundingClientRect();
- const viewportHeight = window.innerHeight;
-
- const visibleTop = Math.max(containerRect.top, 0);
- const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
- const visibleHeight = Math.max(0, visibleBottom - visibleTop);
- const containerHeight = containerRect.height;
-
- const visibilityRatio = visibleHeight / containerHeight;
- if (visibilityRatio >= 0.3) {
- // Consider visible if 30% or more is showing
- visibleIndexes.push(index);
- }
- });
-
- // Check if any visible page is a Beilage page
- for (const index of visibleIndexes) {
- const container = window.currentPageContainers[index];
- if (container && container.id && container.id.includes("beilage-")) {
- return true;
- }
- }
-
- return false;
-}
-
-function updateButtonStates() {
- const prevBtn = document.getElementById("prevPageBtn");
- const nextBtn = document.getElementById("nextPageBtn");
- const beilageBtn = document.getElementById("beilageBtn");
-
- if (prevBtn) {
- if (window.currentActiveIndex <= 0) {
- prevBtn.style.display = "none";
- } else {
- prevBtn.style.display = "flex";
- }
- }
-
- if (nextBtn) {
- if (window.currentActiveIndex >= window.currentPageContainers.length - 1) {
- nextBtn.style.display = "none";
- } else {
- nextBtn.style.display = "flex";
- }
- }
-
- // Update Beilage button based on current location
- if (beilageBtn) {
- const isViewingBeilage = isCurrentlyInBeilageSection();
- const icon = beilageBtn.querySelector("i");
-
- if (isViewingBeilage) {
- // Show "Go to Main Issue" state - use gray styling
- beilageBtn.title = "Zur Hauptausgabe";
- beilageBtn.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";
- if (icon) {
- icon.className = "ri-file-text-line text-lg lg:text-xl";
- }
- } else {
- // Show "Go to Beilage" state - use amber styling
- beilageBtn.title = "Zu Beilage";
- beilageBtn.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";
- if (icon) {
- icon.className = "ri-attachment-line text-lg lg:text-xl";
- }
- }
- }
-}
-
-// Share and citation functions
-function shareCurrentPage() {
- const button = document.getElementById("shareLinkBtn");
-
- // Get current page information
- let pageInfo = "";
-
- // Try to get the currently visible page number from active containers
- if (
- window.currentActiveIndex !== undefined &&
- window.currentPageContainers &&
- window.currentPageContainers[window.currentActiveIndex]
- ) {
- const activeContainer = window.currentPageContainers[window.currentActiveIndex];
- const pageElement = activeContainer.querySelector("[data-page]");
- if (pageElement) {
- const pageNumber = pageElement.getAttribute("data-page");
- pageInfo = `/${pageNumber}`;
- }
- }
-
- // Construct the shareable URL
- const currentUrl = window.location.origin + window.location.pathname + pageInfo;
-
- // Try to use Web Share API if available (mobile browsers)
- if (navigator.share) {
- navigator
- .share({
- title: document.title,
- url: currentUrl,
- })
- .catch((err) => {
- // Fallback to clipboard
- copyToClipboard(currentUrl, button);
- });
- } else {
- // Fallback: copy to clipboard
- copyToClipboard(currentUrl, button);
- }
-}
-
-function copyToClipboard(text, button) {
- if (navigator.clipboard) {
- navigator.clipboard
- .writeText(text)
- .then(() => {
- showSimplePopup(button, "Link kopiert!");
- })
- .catch((err) => {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- });
- } else {
- // Fallback for older browsers
- const textarea = document.createElement("textarea");
- textarea.value = text;
- document.body.appendChild(textarea);
- textarea.select();
- try {
- const successful = document.execCommand("copy");
- showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
- } catch (err) {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- } finally {
- document.body.removeChild(textarea);
- }
- }
-}
-
-function generateCitation() {
- const button = document.getElementById("citationBtn");
-
- // Get current page and issue information
- const issueInfo = document.title || "KGPZ";
-
- // Use clean URL without hash fragments
- let cleanUrl = window.location.origin + window.location.pathname;
-
- // Remove any hash fragments that might still exist
- if (cleanUrl.includes("#")) {
- cleanUrl = cleanUrl.split("#")[0];
- }
-
- // Basic citation format (can be expanded later)
- const currentDate = new Date().toLocaleDateString("de-DE");
- const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}. Digital verfügbar unter: ${cleanUrl} (Zugriff: ${currentDate}).`;
-
- // Copy to clipboard
- if (navigator.clipboard) {
- navigator.clipboard
- .writeText(citation)
- .then(() => {
- showSimplePopup(button, "Zitation kopiert!");
- })
- .catch((err) => {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- });
- } else {
- // Fallback for older browsers
- const textarea = document.createElement("textarea");
- textarea.value = citation;
- document.body.appendChild(textarea);
- textarea.select();
- try {
- const successful = document.execCommand("copy");
- showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
- } catch (err) {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- } finally {
- document.body.removeChild(textarea);
- }
- }
-}
-
-function showSimplePopup(button, message) {
- // Remove any existing popup
- const existingPopup = document.querySelector(".simple-popup");
- if (existingPopup) {
- existingPopup.remove();
- }
-
- // Create popup element
- const popup = document.createElement("div");
- popup.className = "simple-popup";
- popup.textContent = message;
-
- // Style the popup
- popup.style.cssText = `
- position: fixed;
- background: #374151;
- color: white;
- padding: 6px 12px;
- border-radius: 6px;
- font-size: 13px;
- font-weight: 500;
- z-index: 1000;
- pointer-events: none;
- opacity: 0;
- transition: opacity 0.2s ease;
- white-space: nowrap;
- `;
-
- // Position popup next to button
- const buttonRect = button.getBoundingClientRect();
- const viewportHeight = window.innerHeight;
- const viewportWidth = window.innerWidth;
-
- // Calculate position relative to viewport (since we're using fixed positioning)
- let left = buttonRect.left - 10;
- let top = buttonRect.bottom + 8;
-
- // Ensure popup doesn't go off-screen
- const popupWidth = 120; // Estimated popup width
- const popupHeight = 32; // Estimated popup height
-
- // Adjust horizontal position if too far right
- if (left + popupWidth > viewportWidth) {
- left = buttonRect.right - popupWidth + 10;
- }
-
- // Adjust vertical position if too far down (show above button instead)
- if (top + popupHeight > viewportHeight) {
- top = buttonRect.top - popupHeight - 8;
- }
-
- popup.style.left = Math.max(5, left) + "px";
- popup.style.top = Math.max(5, top) + "px";
-
- // Add to page
- document.body.appendChild(popup);
-
- // Fade in
- setTimeout(() => {
- popup.style.opacity = "1";
- }, 10);
-
- // Auto-remove after 2 seconds
- setTimeout(() => {
- popup.style.opacity = "0";
- setTimeout(() => {
- if (popup.parentNode) {
- popup.remove();
- }
- }, 200);
- }, 2000);
-}
-
-// URL navigation functions
-function scrollToPageFromURL() {
- // Skip auto-scrolling during HTMX navigation to prevent random jumps
- if (window.htmxNavigating) {
- return;
- }
-
- let pageNumber = "";
- let targetContainer = null;
-
- // Check if URL ends with a page number (e.g., /1768/42/166)
- const pathParts = window.location.pathname.split("/");
- if (pathParts.length >= 4 && !isNaN(pathParts[pathParts.length - 1])) {
- pageNumber = pathParts[pathParts.length - 1];
-
- // Try to find exact page container first
- targetContainer = document.getElementById(`page-${pageNumber}`);
-
- // If not found, try to find container that contains this page
- if (!targetContainer) {
- // Look for double-spread containers that contain this page
- const containers = document.querySelectorAll(".newspaper-page-container[data-pages]");
- for (const container of containers) {
- const pages = container.getAttribute("data-pages");
- if (pages && pages.split(",").includes(pageNumber)) {
- targetContainer = container;
- break;
- }
- }
- }
-
- // If still not found, try beilage containers
- if (!targetContainer) {
- targetContainer =
- document.getElementById(`beilage-1-page-${pageNumber}`) ||
- document.getElementById(`beilage-2-page-${pageNumber}`) ||
- document.querySelector(`[id*="beilage"][id*="page-${pageNumber}"]`);
- }
- }
-
- if (targetContainer && pageNumber) {
- setTimeout(() => {
- targetContainer.scrollIntoView({
- behavior: "smooth",
- block: "start",
- });
-
- // Highlight the specific page in the table of contents
- markCurrentPageInInhaltsverzeichnis(pageNumber);
- }, 300);
- }
-}
-
-// Page-specific utilities
-function copyPagePermalink(pageNumber, button, isBeilage = false) {
- let pageUrl = "";
- if (isBeilage) {
- // For beilage pages, still use hash for now until beilage URLs are updated
- pageUrl = window.location.origin + window.location.pathname + `#beilage-1-page-${pageNumber}`;
- } else {
- // For regular pages, use the new URL format
- const pathParts = window.location.pathname.split("/");
- if (pathParts.length >= 3) {
- // Current URL format: /year/issue or /year/issue/page
- // New format: /year/issue/page
- const year = pathParts[1];
- const issue = pathParts[2];
- pageUrl = `${window.location.origin}/${year}/${issue}/${pageNumber}`;
- } else {
- // Fallback to current URL with page appended
- pageUrl = window.location.origin + window.location.pathname + `/${pageNumber}`;
- }
- }
-
- const currentUrl = pageUrl;
-
- // Copy to clipboard
- if (navigator.clipboard) {
- navigator.clipboard
- .writeText(currentUrl)
- .then(() => {
- showSimplePopup(button, "Link kopiert!");
- })
- .catch((err) => {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- });
- } else {
- // Fallback for older browsers
- const textarea = document.createElement("textarea");
- textarea.value = currentUrl;
- document.body.appendChild(textarea);
- textarea.select();
- try {
- const successful = document.execCommand("copy");
- showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
- } catch (err) {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- } finally {
- document.body.removeChild(textarea);
- }
- }
-}
-
-function generatePageCitation(pageNumber, button) {
- // Get current issue information
- const issueInfo = document.title || "KGPZ";
-
- // Generate page URL in new format
- const pathParts = window.location.pathname.split("/");
- let pageUrl;
- if (pathParts.length >= 3) {
- const year = pathParts[1];
- const issue = pathParts[2];
- pageUrl = `${window.location.origin}/${year}/${issue}/${pageNumber}`;
- } else {
- pageUrl = `${window.location.origin}${window.location.pathname}/${pageNumber}`;
- }
-
- const currentUrl = pageUrl;
-
- // Basic citation format for specific page
- const currentDate = new Date().toLocaleDateString("de-DE");
- const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}, Seite ${pageNumber}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
-
- // Copy to clipboard
- if (navigator.clipboard) {
- navigator.clipboard
- .writeText(citation)
- .then(() => {
- showSimplePopup(button, "Zitation kopiert!");
- })
- .catch((err) => {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- });
- } else {
- // Fallback for older browsers
- const textarea = document.createElement("textarea");
- textarea.value = citation;
- document.body.appendChild(textarea);
- textarea.select();
- try {
- const successful = document.execCommand("copy");
- showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
- } catch (err) {
- showSimplePopup(button, "Kopieren fehlgeschlagen");
- } finally {
- document.body.removeChild(textarea);
- }
- }
-}
-
-// Initialize scrollspy functionality for author/agent pages
-function initializeScrollspy() {
- // Clean up any existing scrollspy
- cleanupScrollspy();
-
- const sections = document.querySelectorAll(".author-section");
- const navLinks = document.querySelectorAll(".scrollspy-link");
-
- if (sections.length === 0 || navLinks.length === 0) {
- return;
- }
-
- function ensureMarkerVisibility() {
- const slider = document.getElementById("scrollspy-slider");
- const nav = document.getElementById("scrollspy-nav");
-
- if (!slider || !nav || slider.style.opacity === '0') {
- return;
- }
-
- const navRect = nav.getBoundingClientRect();
- const sliderTop = parseFloat(slider.style.top);
- const sliderHeight = parseFloat(slider.style.height);
- const sliderBottom = sliderTop + sliderHeight;
-
- // Check if the marker extends beyond the visible area
- const navScrollTop = nav.scrollTop;
- const navVisibleBottom = navScrollTop + navRect.height;
-
- if (sliderBottom > navVisibleBottom) {
- // Marker extends below visible area - scroll down to show the bottom
- const scrollTarget = sliderBottom - navRect.height + 20; // 20px padding
- nav.scrollTo({
- top: scrollTarget,
- behavior: "smooth"
- });
- } else if (sliderTop < navScrollTop) {
- // Marker extends above visible area - scroll up to show the top
- const scrollTarget = sliderTop - 20; // 20px padding
- nav.scrollTo({
- top: Math.max(0, scrollTarget),
- behavior: "smooth"
- });
- }
- }
-
- function updateActiveLink() {
- const visibleSections = [];
- const viewportTop = window.scrollY;
- const viewportBottom = viewportTop + window.innerHeight;
-
- // Check which sections have any part of their Werke or Beiträge visible
- sections.forEach((section) => {
- const sectionId = section.getAttribute("id");
-
- // Find Werke and Beiträge sections within this author section
- const werkeSection = section.querySelector(".akteur-werke-section");
- const beitraegeSection = section.querySelector(".akteur-beitraege-section");
-
- let isVisible = false;
-
- // Check if any part of Werke section is visible
- if (werkeSection) {
- const werkeRect = werkeSection.getBoundingClientRect();
- const werkeTopVisible = werkeRect.top < window.innerHeight;
- const werkeBottomVisible = werkeRect.bottom > 0;
- if (werkeTopVisible && werkeBottomVisible) {
- isVisible = true;
- }
- }
-
- // Check if any part of Beiträge section is visible
- if (beitraegeSection && !isVisible) {
- const beitraegeRect = beitraegeSection.getBoundingClientRect();
- const beitraegeTopVisible = beitraegeRect.top < window.innerHeight;
- const beitraegeBottomVisible = beitraegeRect.bottom > 0;
- if (beitraegeTopVisible && beitraegeBottomVisible) {
- isVisible = true;
- }
- }
-
- // Fallback: if no Werke/Beiträge sections, check header visibility (for authors without content)
- if (!werkeSection && !beitraegeSection) {
- const headerElement = section.querySelector("div:first-child");
- if (headerElement) {
- const headerRect = headerElement.getBoundingClientRect();
- const headerTopVisible = headerRect.top >= 0;
- const headerBottomVisible = headerRect.bottom <= window.innerHeight;
- if (headerTopVisible && headerBottomVisible) {
- isVisible = true;
- }
- }
- }
-
- if (isVisible) {
- visibleSections.push(sectionId);
- }
- });
-
- // Update highlighting with sliding background animation
- const activeLinks = [];
- const slider = document.getElementById("scrollspy-slider");
-
- // Reset all links to default state (no background, just font weight for active)
- navLinks.forEach((link) => {
- link.classList.remove("font-medium");
-
- const targetId = link.getAttribute("data-target");
- if (visibleSections.includes(targetId)) {
- link.classList.add("font-medium");
- activeLinks.push(link);
- }
- });
-
- // Calculate and animate the sliding background
- if (activeLinks.length > 0 && slider) {
- // Get the nav container for relative positioning
- const nav = document.getElementById("scrollspy-nav");
- const navRect = nav.getBoundingClientRect();
-
- // Calculate the combined area of all active links
- let minTop = Infinity;
- let maxBottom = -Infinity;
-
- activeLinks.forEach((link) => {
- const linkRect = link.getBoundingClientRect();
- const relativeTop = linkRect.top - navRect.top + nav.scrollTop;
- const relativeBottom = relativeTop + linkRect.height;
-
- minTop = Math.min(minTop, relativeTop);
- maxBottom = Math.max(maxBottom, relativeBottom);
- });
-
- // Handle gaps between non-consecutive active links
- let totalHeight = maxBottom - minTop;
-
- // Position and size the slider
- slider.style.top = `${minTop}px`;
- slider.style.height = `${totalHeight}px`;
- slider.style.opacity = '1';
-
- // Ensure the full marker is visible
- setTimeout(() => ensureMarkerVisibility(), 100);
- } else if (slider) {
- // Hide slider when no active links
- slider.style.opacity = '0';
- }
-
- // Implement proportional scrolling to keep active links visible
- if (activeLinks.length > 0) {
- updateSidebarScroll(activeLinks);
- }
- }
-
- function updateActiveLinkImmediate(targetId) {
- // Immediately highlight the clicked link
- const navLinks = document.querySelectorAll(".scrollspy-link");
- const slider = document.getElementById("scrollspy-slider");
-
- // Reset all links
- navLinks.forEach((link) => {
- link.classList.remove("font-medium");
- });
-
- // Highlight the clicked link
- const clickedLink = document.querySelector(`[data-target="${targetId}"]`);
- if (clickedLink) {
- clickedLink.classList.add("font-medium");
-
- // Update slider position for the single clicked link
- if (slider) {
- const nav = document.getElementById("scrollspy-nav");
- const navRect = nav.getBoundingClientRect();
- const linkRect = clickedLink.getBoundingClientRect();
-
- const relativeTop = linkRect.top - navRect.top + nav.scrollTop;
- slider.style.top = `${relativeTop}px`;
- slider.style.height = `${linkRect.height}px`;
- slider.style.opacity = '1';
- }
- }
- }
-
- function updateSidebarScroll(activeLinks) {
- // Skip automatic scrolling during manual navigation
- if (window.scrollspyManualNavigation) return;
-
- const sidebar = document.getElementById("scrollspy-nav");
- if (!sidebar) return;
-
- // Get the first active link as reference
- const firstActiveLink = activeLinks[0];
-
- // Calculate the main content scroll progress
- const documentHeight = Math.max(
- document.body.scrollHeight,
- document.body.offsetHeight,
- document.documentElement.clientHeight,
- document.documentElement.scrollHeight,
- document.documentElement.offsetHeight,
- );
- const viewportHeight = window.innerHeight;
- const maxScroll = documentHeight - viewportHeight;
- const scrollProgress = maxScroll > 0 ? window.scrollY / maxScroll : 0;
-
- // Calculate sidebar scroll dimensions
- const sidebarHeight = sidebar.clientHeight;
- const sidebarScrollHeight = sidebar.scrollHeight;
- const maxSidebarScroll = sidebarScrollHeight - sidebarHeight;
-
- if (maxSidebarScroll > 0) {
- // Calculate proportional scroll position
- const targetSidebarScroll = scrollProgress * maxSidebarScroll;
-
- // Get the position of the first active link within the sidebar
- const linkRect = firstActiveLink.getBoundingClientRect();
- const sidebarRect = sidebar.getBoundingClientRect();
- const linkOffsetInSidebar = linkRect.top - sidebarRect.top + sidebar.scrollTop;
-
- // Calculate the desired position (center the active link in the sidebar viewport)
- const sidebarCenter = sidebarHeight / 2;
- const centeredPosition = linkOffsetInSidebar - sidebarCenter;
-
- // Use a blend of proportional scrolling and centering for smooth behavior
- const blendFactor = 0.7; // 70% proportional, 30% centering
- const finalScrollPosition =
- blendFactor * targetSidebarScroll + (1 - blendFactor) * centeredPosition;
-
- // Clamp to valid scroll range
- const clampedPosition = Math.max(0, Math.min(maxSidebarScroll, finalScrollPosition));
-
- // Apply smooth scrolling, but only if the difference is significant
- const currentScrollTop = sidebar.scrollTop;
- const scrollDifference = Math.abs(clampedPosition - currentScrollTop);
-
- if (scrollDifference > 10) {
- // Only scroll if more than 10px difference
- sidebar.scrollTo({
- top: clampedPosition,
- behavior: "smooth",
- });
- }
- }
- }
-
- // Store scroll handler reference for cleanup
- window.scrollspyScrollHandler = function () {
- clearTimeout(window.scrollspyTimeout);
- window.scrollspyTimeout = setTimeout(() => {
- updateActiveLink();
- updateSidebarScrollToTopButton();
- }, 50);
- };
-
- function updateSidebarScrollToTopButton() {
- const button = document.getElementById("sidebar-scroll-to-top");
- if (!button) return;
-
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
- const viewportHeight = window.innerHeight;
- const shouldShow = scrollTop > viewportHeight * 0.5; // Show after scrolling 50% of viewport
-
- if (shouldShow) {
- button.classList.remove("opacity-0");
- button.classList.add("opacity-100");
- } else {
- button.classList.remove("opacity-100");
- button.classList.add("opacity-0");
- }
- }
-
- // Add scroll listener
- window.addEventListener("scroll", window.scrollspyScrollHandler);
-
- // Store click handlers for cleanup
- window.scrollspyClickHandlers = [];
-
- // Add smooth scroll on link click
- navLinks.forEach((link) => {
- const clickHandler = function (e) {
- e.preventDefault();
- const targetId = this.getAttribute("data-target");
- const target = document.getElementById(targetId);
- if (target) {
- // Immediately update the active link highlighting on click
- updateActiveLinkImmediate(targetId);
-
- // Temporarily disable automatic sidebar scrolling during manual navigation
- window.scrollspyManualNavigation = true;
-
- target.scrollIntoView({
- behavior: "smooth",
- block: "start",
- });
-
- // Re-enable automatic scrolling after navigation completes
- setTimeout(() => {
- window.scrollspyManualNavigation = false;
- // Ensure the full marker is visible after scroll settles
- ensureMarkerVisibility();
- }, 1000);
- }
- };
-
- window.scrollspyClickHandlers.push({ link, handler: clickHandler });
- link.addEventListener("click", clickHandler);
- });
-
- // Initial active link update
- updateActiveLink();
-
- // Initial scroll-to-top button update
- updateSidebarScrollToTopButton();
-}
-
-// Cleanup scrollspy functionality
-function cleanupScrollspy() {
- // Remove scroll listener
- if (window.scrollspyScrollHandler) {
- window.removeEventListener("scroll", window.scrollspyScrollHandler);
- window.scrollspyScrollHandler = null;
- }
-
- // Clear timeout
- if (window.scrollspyTimeout) {
- clearTimeout(window.scrollspyTimeout);
- window.scrollspyTimeout = null;
- }
-
- // Remove click handlers
- if (window.scrollspyClickHandlers) {
- window.scrollspyClickHandlers.forEach(({ link, handler }) => {
- link.removeEventListener("click", handler);
- });
- window.scrollspyClickHandlers = null;
- }
-
- // Reset slider
- const slider = document.getElementById("scrollspy-slider");
- if (slider) {
- slider.style.opacity = '0';
- slider.style.height = '0';
- }
-
- // Reset manual navigation flag
- window.scrollspyManualNavigation = false;
-}
-
-// Initialize newspaper layout functionality
-function initializeNewspaperLayout() {
- // Initialize page highlighting
- initializePageHighlighting();
-
- // Initialize page tracking
- initializePageTracking();
-
- // Set up scroll handler
- window.addEventListener("scroll", function () {
- clearTimeout(window.scrollTimeout);
- window.scrollTimeout = setTimeout(() => {
- checkAndHighlightVisiblePages();
- updateButtonStates(); // Update button states including Beilage toggle
- }, 50);
- });
-
- // Initialize URL-based page navigation
- scrollToPageFromURL();
-
- // Set up keyboard shortcuts
- document.addEventListener("keydown", function (e) {
- if (e.key === "Escape") {
- closeModal();
- }
- });
-}
-
// Export functions for global access
window.enlargePage = enlargePage;
window.closeModal = closeModal;
@@ -1369,802 +101,41 @@ window.generateCitation = generateCitation;
window.copyPagePermalink = copyPagePermalink;
window.generatePageCitation = generatePageCitation;
-// Apply page-specific backdrop styling based on URL
-function applyPageBackdrop() {
- const path = window.location.pathname;
- const body = document.body;
-
- // Remove any existing page-specific classes
- body.classList.remove('page-akteure', 'page-ausgabe', 'page-search', 'page-ort', 'page-kategorie', 'page-piece', 'page-edition');
-
- // Determine page type from URL path and apply appropriate class
- if (path.includes('/akteure/') || path.includes('/autoren')) {
- body.classList.add('page-akteure');
- } else if (path.match(/\/\d{4}\/\d+/)) { // Issue URLs like /1771/42 or /1771/42/166
- body.classList.add('page-ausgabe');
- } else if (path.includes('/search') || path.includes('/suche')) {
- body.classList.add('page-search');
- } else if (path.includes('/ort/')) {
- body.classList.add('page-ort');
- } else if (path.includes('/kategorie/')) {
- body.classList.add('page-kategorie');
- } else if (path.includes('/beitrag/')) {
- body.classList.add('page-piece');
- } else if (path.includes('/edition')) {
- body.classList.add('page-edition');
- }
-}
-
// INFO: This is intended to be called once on website load
function setup() {
- setup_xslt();
- setup_templates();
-
// Apply page-specific backdrop styling
applyPageBackdrop();
+ // Update citation links on initial load
+ updateCitationLinks();
+
// Initialize newspaper layout if present
if (document.querySelector(".newspaper-page-container")) {
initializeNewspaperLayout();
}
- // Initialize scrollspy if author/agent sections are present
- if (document.querySelector(".author-section")) {
- initializeScrollspy();
- }
-
- // Set up HTMX event handlers
- htmx.on("htmx:load", function (_) {
- // INFO: We can instead use afterSettle; and also clear the map with
- // xslt_processors.clear();
- setup_xslt();
- });
+ // Akteure scrollspy web component will auto-initialize when present in template
// HTMX event handling for newspaper layout, scrollspy, and scroll-to-top button
- document.body.addEventListener("htmx:afterSwap", function (event) {
+ let htmxAfterSwapHandler = function (event) {
// Apply page-specific backdrop styling after navigation
applyPageBackdrop();
+ // Update citation links after navigation
+ updateCitationLinks();
- // Prevent auto-scrolling during HTMX navigation
- window.htmxNavigating = true;
-
- // Scroll to top for normal page navigation (not URL anchors)
- const currentUrl = window.location.pathname;
- const hasPageAnchor = currentUrl.match(/\/\d+$/); // Ends with page number like /1768/42/166
- const isAkteureNavigation = currentUrl.includes('/akteure/') || currentUrl.includes('/autoren');
-
- if (!hasPageAnchor && isAkteureNavigation) {
- // Small delay to ensure DOM is ready
- setTimeout(() => {
- window.scrollTo({
- top: 0,
- behavior: "instant" // Use instant instead of smooth to avoid conflicts
- });
- }, 50);
- }
-
+ // Use shorter delay since afterSettle ensures DOM is ready
setTimeout(() => {
if (document.querySelector(".newspaper-page-container")) {
initializeNewspaperLayout();
}
- if (document.querySelector(".author-section")) {
- initializeScrollspy();
- }
- // Reassess scroll-to-top button visibility after page swap
- const scrollToTopButton = document.querySelector("scroll-to-top-button");
- if (scrollToTopButton) {
- scrollToTopButton.reassessScrollPosition();
- }
+ }, 50);
+ };
- // Re-enable auto-scrolling after a delay
- setTimeout(() => {
- window.htmxNavigating = false;
- }, 500);
- }, 100);
- });
+ let htmxBeforeRequestHandler = function (event) {};
- // Remove duplicate event handlers to prevent multiple initialization
- document.body.addEventListener("htmx:beforeRequest", function (event) {
- // Set flag to prevent auto-scrolling during navigation
- window.htmxNavigating = true;
- });
+ document.body.addEventListener("htmx:afterSettle", htmxAfterSwapHandler);
+ document.body.addEventListener("htmx:afterSettle", updateCitationLinks);
+ document.body.addEventListener("htmx:beforeRequest", htmxBeforeRequestHandler);
}
-// Single Page Viewer Web Component
-class SinglePageViewer extends HTMLElement {
- constructor() {
- super();
- // No shadow DOM - use regular DOM to allow Tailwind CSS
- this.resizeObserver = null;
- }
-
- // Dynamically detect sidebar width in pixels
- detectSidebarWidth() {
- // Find the actual sidebar element in the current layout
- const sidebar = document.querySelector('.lg\\:w-1\\/4, .lg\\:w-1\\/3, [class*="lg:w-1/"]');
-
- if (sidebar) {
- const sidebarRect = sidebar.getBoundingClientRect();
- const sidebarWidth = sidebarRect.width;
- console.log("Detected sidebar width:", sidebarWidth, "px");
- return `${sidebarWidth}px`;
- }
-
- // Fallback: calculate based on viewport width and responsive breakpoints
- const viewportWidth = window.innerWidth;
-
- if (viewportWidth < 1024) {
- // Below lg breakpoint - no sidebar space needed
- return "0px";
- } else if (viewportWidth < 1280) {
- // lg breakpoint: assume 1/4 of viewport (similar to both layouts)
- return `${Math.floor(viewportWidth * 0.25)}px`;
- } else {
- // xl breakpoint: assume 1/5 of viewport (narrower on larger screens)
- return `${Math.floor(viewportWidth * 0.2)}px`;
- }
- }
-
- connectedCallback() {
- // Detect sidebar width dynamically
- const sidebarWidth = this.detectSidebarWidth();
-
- this.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
![Klicken zum Schließen]()
-
-
-
-
- `;
-
- // Set up resize observer to handle window resizing
- this.setupResizeObserver();
- }
-
- // Set up resize observer to dynamically update sidebar width
- setupResizeObserver() {
- // Clean up existing observer
- if (this.resizeObserver) {
- this.resizeObserver.disconnect();
- }
-
- // Create new resize observer
- this.resizeObserver = new ResizeObserver(() => {
- this.updateSidebarWidth();
- });
-
- // Observe window resizing by watching the document body
- this.resizeObserver.observe(document.body);
- }
-
- // Update sidebar width when window is resized
- updateSidebarWidth() {
- const sidebarSpacer = this.querySelector("#sidebar-spacer");
- if (sidebarSpacer && !sidebarSpacer.style.width.includes("0px")) {
- // Only update if sidebar is not collapsed (not 0px width)
- const newWidth = this.detectSidebarWidth();
- sidebarSpacer.style.width = newWidth;
- console.log("Updated sidebar width to:", newWidth);
- }
- }
-
- show(imgSrc, imgAlt, pageNumber, isBeilage = false, targetPage = 0, partNumber = null) {
- const img = this.querySelector("#single-page-image");
- const pageNumberSpan = this.querySelector("#page-number");
- const pageIconSpan = this.querySelector("#page-icon");
- const pageIndicator = this.querySelector("#page-indicator");
-
- img.src = imgSrc;
- img.alt = imgAlt;
-
- // Store current page info for button actions
- this.currentPageNumber = pageNumber;
- this.currentIsBeilage = isBeilage;
- this.currentPartNumber = partNumber;
-
- // Get issue context from document title or URL
- const issueContext = this.getIssueContext(pageNumber);
-
- // Set page number with issue context in the box
- pageNumberSpan.innerHTML = issueContext ? `${issueContext}, ${pageNumber}` : `${pageNumber}`;
-
- // Add red dot if this is the target page
- if (targetPage && pageNumber === targetPage) {
- pageNumberSpan.style.position = "relative";
- // Remove any existing red dot
- const existingDot = pageNumberSpan.querySelector(".target-page-dot");
- if (existingDot) {
- existingDot.remove();
- }
- // Add new red dot
- const redDot = document.createElement("span");
- redDot.className =
- "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10";
- redDot.title = "verlinkte Seite";
- pageNumberSpan.appendChild(redDot);
- }
-
- // Set page icon or part number based on view type
- if (partNumber !== null) {
- // Piece view: Show part number instead of icon
- pageIconSpan.innerHTML = `
${partNumber}. Teil`;
- } else {
- // Issue view: Show icon based on position and type
- const iconType = this.determinePageIconType(pageNumber, isBeilage);
- pageIconSpan.innerHTML = this.getPageIconHTML(iconType);
- }
-
- // Page indicator styling is now consistent (white background)
-
- // Update navigation button visibility
- this.updateNavigationButtons();
-
- this.style.display = "block";
-
- // Prevent background scrolling but allow scrolling within the viewer
- document.body.style.overflow = "hidden";
-
- // Mark current page in Inhaltsverzeichnis
- markCurrentPageInInhaltsverzeichnis(pageNumber);
- }
-
- close() {
- this.style.display = "none";
-
- // Restore background scrolling
- document.body.style.overflow = "";
- }
-
- // Clean up component completely
- destroy() {
- // Clean up resize observer
- if (this.resizeObserver) {
- this.resizeObserver.disconnect();
- this.resizeObserver = null;
- }
-
- // Restore background scrolling
- document.body.style.overflow = "";
-
- // Remove the component from DOM
- if (this.parentNode) {
- this.parentNode.removeChild(this);
- }
- }
-
- // Determine page icon type based on page position and whether it's beilage
- determinePageIconType(pageNumber, isBeilage) {
- // Get all page containers to determine position
- const containerSelector = isBeilage
- ? '.newspaper-page-container[data-beilage="true"]'
- : ".newspaper-page-container:not([data-beilage])";
- const pageContainers = Array.from(document.querySelectorAll(containerSelector));
-
- // Extract page numbers and sort them
- const allPages = pageContainers
- .map((container) => {
- const pageAttr = container.getAttribute("data-page-container");
- return pageAttr ? parseInt(pageAttr) : null;
- })
- .filter((p) => p !== null)
- .sort((a, b) => a - b);
-
- if (allPages.length === 0) {
- return "first";
- }
-
- const firstPage = allPages[0];
- const lastPage = allPages[allPages.length - 1];
-
- // Same logic as Go determinePageIcon function
- if (pageNumber === firstPage) {
- return "first"; // Front page - normal icon
- } else if (pageNumber === lastPage) {
- return "last"; // Back page - mirrored icon
- } else {
- // For middle pages in newspaper layout
- if (pageNumber === firstPage + 1) {
- return "even"; // Page 2 - black + mirrored grey
- } else if (pageNumber === lastPage - 1) {
- return "odd"; // Page 3 - grey + black
- } else {
- // For newspapers with more than 4 pages, use alternating pattern
- if (pageNumber % 2 === 0) {
- return "even";
- } else {
- return "odd";
- }
- }
- }
- }
-
- // Generate page icon HTML based on type (same as Go PageIcon function)
- getPageIconHTML(iconType) {
- const baseClass = "ri-file-text-line text-lg";
-
- switch (iconType) {
- case "first":
- return `
`;
- case "last":
- return `
`;
- case "even":
- return `
`;
- case "odd":
- return `
`;
- default:
- return `
`;
- }
- }
-
- // Share current page
- shareCurrentPage() {
- if (typeof copyPagePermalink === "function") {
- // Use the actual button element
- const shareBtn = this.querySelector("#share-btn");
- copyPagePermalink(this.currentPageNumber, shareBtn, this.currentIsBeilage);
- }
- }
-
- // Generate citation for current page
- generatePageCitation() {
- if (typeof generatePageCitation === "function") {
- // Use the actual button element
- const citeBtn = this.querySelector("#cite-btn");
- generatePageCitation(this.currentPageNumber, citeBtn);
- }
- }
-
- // Update navigation button visibility based on available pages
- updateNavigationButtons() {
- const prevBtn = this.querySelector("#prev-page-btn");
- const nextBtn = this.querySelector("#next-page-btn");
-
- const { prevPage, nextPage } = this.getAdjacentPages();
-
- // Enable/disable previous page button
- if (prevPage !== null) {
- prevBtn.disabled = false;
- prevBtn.className = prevBtn.className.replace("opacity-50 cursor-not-allowed", "");
- prevBtn.className = prevBtn.className.replace(
- "bg-gray-50 text-gray-400",
- "bg-gray-100 text-gray-700",
- );
- } else {
- prevBtn.disabled = true;
- if (!prevBtn.className.includes("opacity-50")) {
- prevBtn.className += " opacity-50 cursor-not-allowed";
- }
- prevBtn.className = prevBtn.className.replace(
- "bg-gray-100 text-gray-700",
- "bg-gray-50 text-gray-400",
- );
- }
-
- // Enable/disable next page button
- if (nextPage !== null) {
- nextBtn.disabled = false;
- nextBtn.className = nextBtn.className.replace("opacity-50 cursor-not-allowed", "");
- nextBtn.className = nextBtn.className.replace(
- "bg-gray-50 text-gray-400",
- "bg-gray-100 text-gray-700",
- );
- } else {
- nextBtn.disabled = true;
- if (!nextBtn.className.includes("opacity-50")) {
- nextBtn.className += " opacity-50 cursor-not-allowed";
- }
- nextBtn.className = nextBtn.className.replace(
- "bg-gray-100 text-gray-700",
- "bg-gray-50 text-gray-400",
- );
- }
- }
-
- // Get previous and next page numbers
- getAdjacentPages() {
- // Get all page containers of the same type (main or beilage)
- let containerSelector;
- if (this.currentIsBeilage) {
- containerSelector = '.newspaper-page-container[data-beilage="true"]';
- } else {
- containerSelector = ".newspaper-page-container:not([data-beilage])";
- }
-
- const pageContainers = Array.from(document.querySelectorAll(containerSelector));
- console.log(
- "Found containers:",
- pageContainers.length,
- "for",
- this.currentIsBeilage ? "beilage" : "main",
- );
-
- // Extract page numbers and sort them
- const allPages = pageContainers
- .map((container) => {
- const pageAttr = container.getAttribute("data-page-container");
- const pageNum = pageAttr ? parseInt(pageAttr) : null;
- console.log("Container page:", pageAttr, "parsed:", pageNum);
- return pageNum;
- })
- .filter((p) => p !== null)
- .sort((a, b) => a - b);
-
- console.log("All pages found:", allPages);
- console.log("Current page:", this.currentPageNumber);
-
- const currentIndex = allPages.indexOf(this.currentPageNumber);
- console.log("Current index:", currentIndex);
-
- let prevPage = null;
- let nextPage = null;
-
- if (currentIndex > 0) {
- prevPage = allPages[currentIndex - 1];
- }
-
- if (currentIndex < allPages.length - 1) {
- nextPage = allPages[currentIndex + 1];
- }
-
- console.log("Adjacent pages - prev:", prevPage, "next:", nextPage);
- return { prevPage, nextPage };
- }
-
- // Navigate to previous page
- goToPreviousPage() {
- const { prevPage } = this.getAdjacentPages();
- if (prevPage !== null) {
- this.navigateToPage(prevPage);
- }
- }
-
- // Navigate to next page
- goToNextPage() {
- const { nextPage } = this.getAdjacentPages();
- if (nextPage !== null) {
- this.navigateToPage(nextPage);
- }
- }
-
- // Navigate to a specific page
- navigateToPage(pageNumber) {
- // Find the image element for the target page
- const containerSelector = this.currentIsBeilage
- ? '.newspaper-page-container[data-beilage="true"]'
- : ".newspaper-page-container:not([data-beilage])";
- const targetContainer = document.querySelector(
- `${containerSelector}[data-page-container="${pageNumber}"]`,
- );
-
- if (targetContainer) {
- const imgElement = targetContainer.querySelector(".newspaper-page-image");
- if (imgElement) {
- // Determine part number for piece view
- let newPartNumber = null;
- if (this.currentPartNumber !== null) {
- // We're in piece view, try to find the part number for the new page
- newPartNumber = this.getPartNumberForPage(pageNumber);
- }
-
- // Update the current view with the new page
- this.show(
- imgElement.src,
- imgElement.alt,
- pageNumber,
- this.currentIsBeilage,
- 0,
- newPartNumber,
- );
- }
- }
- }
-
- // Get part number for a specific page in piece view
- getPartNumberForPage(pageNumber) {
- // Try to find the part number from the page container in piece view
- const pageContainer = document.querySelector(`[data-page-container="${pageNumber}"]`);
- if (pageContainer) {
- const partNumberElement = pageContainer.querySelector(".part-number");
- if (partNumberElement) {
- // Extract just the number from "X. Teil" format
- const match = partNumberElement.textContent.match(/(\d+)\./);
- if (match) {
- return parseInt(match[1]);
- }
- }
- }
-
- // Fallback: if we can't find it, return null to show icon instead
- return null;
- }
-
- // Toggle sidebar visibility
- toggleSidebar() {
- const sidebarSpacer = this.querySelector("#sidebar-spacer");
- const toggleBtn = this.querySelector("#sidebar-toggle-btn");
- const toggleIcon = toggleBtn.querySelector("i");
-
- // Check if sidebar is currently collapsed by looking at width
- const currentWidth = sidebarSpacer.style.width;
- const isCollapsed = currentWidth === "0px" || currentWidth === "0";
-
- console.log("Current state - isCollapsed:", isCollapsed);
- console.log("Current width:", currentWidth);
-
- if (isCollapsed) {
- // Sidebar is collapsed, expand it
- const fullWidth = this.detectSidebarWidth();
- sidebarSpacer.style.width = fullWidth;
-
- // Update button to normal state (sidebar visible)
- toggleBtn.className =
- "w-10 h-10 bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer";
- toggleIcon.className = "ri-sidebar-fold-line text-lg font-bold";
- toggleBtn.title = "Inhaltsverzeichnis ausblenden";
- console.log("Expanding sidebar to:", fullWidth);
- } else {
- // Sidebar is expanded, collapse it
- sidebarSpacer.style.width = "0px";
-
- // Update button to active state (sidebar hidden - orange highlight)
- toggleBtn.className =
- "w-10 h-10 bg-orange-100 hover:bg-orange-200 text-orange-700 border border-orange-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer";
- toggleIcon.className = "ri-sidebar-unfold-line text-lg font-bold";
- toggleBtn.title = "Inhaltsverzeichnis einblenden";
- console.log("Collapsing sidebar");
- }
-
- console.log("New width:", sidebarSpacer.style.width);
- }
-
- // Extract issue context from document title, URL, or page container
- getIssueContext(pageNumber) {
- // Determine if we're in a piece view (beitrag) or issue view (ausgabe)
- const path = window.location.pathname;
- const isPieceView = path.includes("/beitrag/");
-
- if (isPieceView) {
- // For piece view: Return full format "1765 Nr. 2"
- // Try to get context from page container first (for piece view)
- const pageContainer = document.querySelector(`[data-page-container="${pageNumber}"]`);
- if (pageContainer) {
- // Look for existing page indicator with context
- const pageIndicator = pageContainer.querySelector(".page-indicator");
- if (pageIndicator) {
- const text = pageIndicator.textContent.trim();
- // Extract full date and issue from text like "3.7.1767 Nr. 53, 213"
- const fullDateMatch = text.match(/(\d{1,2}\.\d{1,2}\.\d{4}\s+Nr\.\s+\d+)/);
- if (fullDateMatch) {
- return fullDateMatch[1];
- }
- // Fallback: Extract year and issue from text like "1768 Nr. 20, 79"
- const yearMatch = text.match(/(\d{4})\s+Nr\.\s+(\d+)/);
- if (yearMatch) {
- return `${yearMatch[1]} Nr. ${yearMatch[2]}`;
- }
- }
- }
-
- // Fallback: Try to extract from document title
- const title = document.title;
- const titleMatch = title.match(/(\d{4}).*Nr\.\s*(\d+)/);
- if (titleMatch) {
- return `${titleMatch[1]} Nr. ${titleMatch[2]}`;
- }
- } else {
- // For issue view: Return just empty string (page number only)
- return "";
- }
-
- // Final fallback: Try to extract from URL path
- const urlMatch = path.match(/\/(\d{4})\/(\d+)/);
- if (urlMatch) {
- return isPieceView ? `${urlMatch[1]} Nr. ${urlMatch[2]}` : "";
- }
-
- // Fallback - try to get from any visible page context
- const anyPageIndicator = document.querySelector(".page-indicator");
- if (anyPageIndicator) {
- const text = anyPageIndicator.textContent.trim();
- const match = text.match(/(\d{4})\s+Nr\.\s+(\d+)/);
- if (match) {
- return `${match[1]} Nr. ${match[2]}`;
- }
- }
-
- // Ultimate fallback
- return "KGPZ";
- }
-}
-
-// Register the web component
-customElements.define("single-page-viewer", SinglePageViewer);
-
-// Clean up single page viewer on HTMX navigation
-document.body.addEventListener("htmx:beforeRequest", function (event) {
- // Find any active single page viewer
- const viewer = document.querySelector("single-page-viewer");
- if (viewer && viewer.style.display !== "none") {
- console.log("Cleaning up single page viewer before HTMX navigation");
- viewer.destroy();
- }
-
- // Clean up scrollspy before navigating
- cleanupScrollspy();
-});
-
-// Also clean up on page unload as fallback
-window.addEventListener("beforeunload", function () {
- const viewer = document.querySelector("single-page-viewer");
- if (viewer) {
- viewer.destroy();
- }
-});
-
-// Scroll to Top Web Component
-class ScrollToTopButton extends HTMLElement {
- constructor() {
- super();
- this.isVisible = false;
- this.scrollHandler = null;
- }
-
- connectedCallback() {
- // Create the button without shadow DOM so Tailwind works
- this.innerHTML = `
-
- `;
-
- // Set up scroll listener
- this.scrollHandler = () => {
- this.handleScroll();
- };
-
- window.addEventListener('scroll', this.scrollHandler);
-
- // Initial check
- this.handleScroll();
- }
-
- disconnectedCallback() {
- // Clean up event listener
- if (this.scrollHandler) {
- window.removeEventListener('scroll', this.scrollHandler);
- this.scrollHandler = null;
- }
- }
-
- // Method to reassess scroll position (called after HTMX swaps)
- reassessScrollPosition() {
- // Small delay to ensure DOM is settled after HTMX swap
- setTimeout(() => {
- this.handleScroll();
- }, 100);
- }
-
- handleScroll() {
- const button = this.querySelector('#scroll-to-top-btn');
- if (!button) return;
-
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
- const viewportHeight = window.innerHeight;
- const shouldShow = scrollTop > viewportHeight;
-
- if (shouldShow && !this.isVisible) {
- // Show button
- this.isVisible = true;
- button.classList.remove('opacity-0', 'pointer-events-none');
- button.classList.add('opacity-100', 'pointer-events-auto');
- } else if (!shouldShow && this.isVisible) {
- // Hide button
- this.isVisible = false;
- button.classList.remove('opacity-100', 'pointer-events-auto');
- button.classList.add('opacity-0', 'pointer-events-none');
- }
- }
-
- scrollToTop() {
- window.scrollTo({
- top: 0,
- behavior: 'smooth'
- });
- }
-}
-
-// Register the scroll to top component
-customElements.define('scroll-to-top-button', ScrollToTopButton);
-
export { setup };
diff --git a/views/transform/scroll-to-top.js b/views/transform/scroll-to-top.js
new file mode 100644
index 0000000..bf05d7e
--- /dev/null
+++ b/views/transform/scroll-to-top.js
@@ -0,0 +1,94 @@
+// ===========================
+// SCROLL TO TOP COMPONENT
+// ===========================
+
+// Scroll to Top Web Component
+export class ScrollToTopButton extends HTMLElement {
+ constructor() {
+ super();
+ this.isVisible = false;
+ this.scrollHandler = null;
+ this.htmxAfterSwapHandler = null;
+ }
+
+ connectedCallback() {
+ // Create the button without shadow DOM so Tailwind works
+ this.innerHTML = `
+
+ `;
+
+ // Set up scroll listener
+ this.scrollHandler = () => {
+ this.handleScroll();
+ };
+
+ // Set up HTMX event listener
+ this.htmxAfterSwapHandler = () => {
+ this.reassessScrollPosition();
+ };
+
+ window.addEventListener('scroll', this.scrollHandler);
+ document.body.addEventListener('htmx:afterSettle', this.htmxAfterSwapHandler);
+
+ // Initial check
+ this.handleScroll();
+ }
+
+ disconnectedCallback() {
+ // Clean up event listeners
+ if (this.scrollHandler) {
+ window.removeEventListener('scroll', this.scrollHandler);
+ this.scrollHandler = null;
+ }
+
+ if (this.htmxAfterSwapHandler) {
+ document.body.removeEventListener('htmx:afterSettle', this.htmxAfterSwapHandler);
+ this.htmxAfterSwapHandler = null;
+ }
+ }
+
+ // Method to reassess scroll position (called after HTMX swaps)
+ reassessScrollPosition() {
+ // Small delay to ensure DOM is settled after HTMX swap
+ setTimeout(() => {
+ this.handleScroll();
+ }, 100);
+ }
+
+ handleScroll() {
+ const button = this.querySelector('#scroll-to-top-btn');
+ if (!button) return;
+
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ const viewportHeight = window.innerHeight;
+ const shouldShow = scrollTop > viewportHeight;
+
+ if (shouldShow && !this.isVisible) {
+ // Show button
+ this.isVisible = true;
+ button.classList.remove('opacity-0', 'pointer-events-none');
+ button.classList.add('opacity-100', 'pointer-events-auto');
+ } else if (!shouldShow && this.isVisible) {
+ // Hide button
+ this.isVisible = false;
+ button.classList.remove('opacity-100', 'pointer-events-auto');
+ button.classList.add('opacity-0', 'pointer-events-none');
+ }
+ }
+
+ scrollToTop() {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ }
+}
+
+// Register the scroll to top component
+customElements.define('scroll-to-top-button', ScrollToTopButton);
\ No newline at end of file
diff --git a/views/transform/single-page-viewer.js b/views/transform/single-page-viewer.js
new file mode 100644
index 0000000..e2045ab
--- /dev/null
+++ b/views/transform/single-page-viewer.js
@@ -0,0 +1,609 @@
+// ===========================
+// SINGLE PAGE VIEWER COMPONENT
+// ===========================
+
+// Single Page Viewer Web Component
+export class SinglePageViewer extends HTMLElement {
+ constructor() {
+ super();
+ // No shadow DOM - use regular DOM to allow Tailwind CSS
+ this.resizeObserver = null;
+ }
+
+ // Dynamically detect sidebar width in pixels
+ detectSidebarWidth() {
+ // Find the actual sidebar element in the current layout
+ const sidebar = document.querySelector('.lg\\:w-1\\/4, .lg\\:w-1\\/3, [class*="lg:w-1/"]');
+
+ if (sidebar) {
+ const sidebarRect = sidebar.getBoundingClientRect();
+ const sidebarWidth = sidebarRect.width;
+ console.log("Detected sidebar width:", sidebarWidth, "px");
+ return `${sidebarWidth}px`;
+ }
+
+ // Fallback: calculate based on viewport width and responsive breakpoints
+ const viewportWidth = window.innerWidth;
+
+ if (viewportWidth < 1024) {
+ // Below lg breakpoint - no sidebar space needed
+ return "0px";
+ } else if (viewportWidth < 1280) {
+ // lg breakpoint: assume 1/4 of viewport (similar to both layouts)
+ return `${Math.floor(viewportWidth * 0.25)}px`;
+ } else {
+ // xl breakpoint: assume 1/5 of viewport (narrower on larger screens)
+ return `${Math.floor(viewportWidth * 0.2)}px`;
+ }
+ }
+
+ connectedCallback() {
+ // Detect sidebar width dynamically
+ const sidebarWidth = this.detectSidebarWidth();
+
+ this.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![Klicken zum Schließen]()
+
+
+
+
+ `;
+
+ // Set up resize observer to handle window resizing
+ this.setupResizeObserver();
+ }
+
+ // Set up resize observer to dynamically update sidebar width
+ setupResizeObserver() {
+ // Clean up existing observer
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ }
+
+ // Create new resize observer
+ this.resizeObserver = new ResizeObserver(() => {
+ this.updateSidebarWidth();
+ });
+
+ // Observe window resizing by watching the document body
+ this.resizeObserver.observe(document.body);
+ }
+
+ // Update sidebar width when window is resized
+ updateSidebarWidth() {
+ const sidebarSpacer = this.querySelector("#sidebar-spacer");
+ if (sidebarSpacer && !sidebarSpacer.style.width.includes("0px")) {
+ // Only update if sidebar is not collapsed (not 0px width)
+ const newWidth = this.detectSidebarWidth();
+ sidebarSpacer.style.width = newWidth;
+ console.log("Updated sidebar width to:", newWidth);
+ }
+ }
+
+ show(imgSrc, imgAlt, pageNumber, isBeilage = false, targetPage = 0, partNumber = null) {
+ const img = this.querySelector("#single-page-image");
+ const pageNumberSpan = this.querySelector("#page-number");
+ const pageIconSpan = this.querySelector("#page-icon");
+ const pageIndicator = this.querySelector("#page-indicator");
+
+ img.src = imgSrc;
+ img.alt = imgAlt;
+
+ // Store current page info for button actions
+ this.currentPageNumber = pageNumber;
+ this.currentIsBeilage = isBeilage;
+ this.currentPartNumber = partNumber;
+
+ // Get issue context from document title or URL
+ const issueContext = this.getIssueContext(pageNumber);
+
+ // Set page number with issue context in the box
+ pageNumberSpan.innerHTML = issueContext ? `${issueContext}, ${pageNumber}` : `${pageNumber}`;
+
+ // Add red dot if this is the target page
+ if (targetPage && pageNumber === targetPage) {
+ pageNumberSpan.style.position = "relative";
+ // Remove any existing red dot
+ const existingDot = pageNumberSpan.querySelector(".target-page-dot");
+ if (existingDot) {
+ existingDot.remove();
+ }
+ // Add new red dot
+ const redDot = document.createElement("span");
+ redDot.className =
+ "target-page-dot absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full z-10";
+ redDot.title = "verlinkte Seite";
+ pageNumberSpan.appendChild(redDot);
+ }
+
+ // Set page icon or part number based on view type
+ if (partNumber !== null) {
+ // Piece view: Show part number instead of icon
+ pageIconSpan.innerHTML = `
${partNumber}. Teil`;
+ } else {
+ // Issue view: Show icon based on position and type
+ const iconType = this.determinePageIconType(pageNumber, isBeilage);
+ pageIconSpan.innerHTML = this.getPageIconHTML(iconType);
+ }
+
+ // Page indicator styling is now consistent (white background)
+
+ // Update navigation button visibility
+ this.updateNavigationButtons();
+
+ this.style.display = "block";
+
+ // Prevent background scrolling but allow scrolling within the viewer
+ document.body.style.overflow = "hidden";
+ }
+
+ close() {
+ this.style.display = "none";
+
+ // Restore background scrolling
+ document.body.style.overflow = "";
+ }
+
+ disconnectedCallback() {
+ // Clean up resize observer
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ this.resizeObserver = null;
+ }
+
+ // Restore background scrolling
+ document.body.style.overflow = "";
+ }
+
+ // Determine page icon type based on page position and whether it's beilage
+ determinePageIconType(pageNumber, isBeilage) {
+ // Get all page containers to determine position
+ const containerSelector = isBeilage
+ ? '.newspaper-page-container[data-beilage="true"]'
+ : ".newspaper-page-container:not([data-beilage])";
+ const pageContainers = Array.from(document.querySelectorAll(containerSelector));
+
+ // Extract page numbers and sort them
+ const allPages = pageContainers
+ .map((container) => {
+ const pageAttr = container.getAttribute("data-page-container");
+ return pageAttr ? parseInt(pageAttr) : null;
+ })
+ .filter((p) => p !== null)
+ .sort((a, b) => a - b);
+
+ if (allPages.length === 0) {
+ return "first";
+ }
+
+ const firstPage = allPages[0];
+ const lastPage = allPages[allPages.length - 1];
+
+ // Same logic as Go determinePageIcon function
+ if (pageNumber === firstPage) {
+ return "first"; // Front page - normal icon
+ } else if (pageNumber === lastPage) {
+ return "last"; // Back page - mirrored icon
+ } else {
+ // For middle pages in newspaper layout
+ if (pageNumber === firstPage + 1) {
+ return "even"; // Page 2 - black + mirrored grey
+ } else if (pageNumber === lastPage - 1) {
+ return "odd"; // Page 3 - grey + black
+ } else {
+ // For newspapers with more than 4 pages, use alternating pattern
+ if (pageNumber % 2 === 0) {
+ return "even";
+ } else {
+ return "odd";
+ }
+ }
+ }
+ }
+
+ // Generate page icon HTML based on type (same as Go PageIcon function)
+ getPageIconHTML(iconType) {
+ const baseClass = "ri-file-text-line text-lg";
+
+ switch (iconType) {
+ case "first":
+ return `
`;
+ case "last":
+ return `
`;
+ case "even":
+ return `
`;
+ case "odd":
+ return `
`;
+ default:
+ return `
`;
+ }
+ }
+
+ // Share current page
+ shareCurrentPage() {
+ if (typeof copyPagePermalink === "function") {
+ // Use the actual button element
+ const shareBtn = this.querySelector("#share-btn");
+ copyPagePermalink(this.currentPageNumber, shareBtn, this.currentIsBeilage);
+ }
+ }
+
+ // Generate citation for current page
+ generatePageCitation() {
+ if (typeof generatePageCitation === "function") {
+ // Use the actual button element
+ const citeBtn = this.querySelector("#cite-btn");
+ generatePageCitation(this.currentPageNumber, citeBtn);
+ }
+ }
+
+ // Update navigation button visibility based on available pages
+ updateNavigationButtons() {
+ const prevBtn = this.querySelector("#prev-page-btn");
+ const nextBtn = this.querySelector("#next-page-btn");
+
+ const { prevPage, nextPage } = this.getAdjacentPages();
+
+ // Enable/disable previous page button
+ if (prevPage !== null) {
+ prevBtn.disabled = false;
+ prevBtn.className = prevBtn.className.replace("opacity-50 cursor-not-allowed", "");
+ prevBtn.className = prevBtn.className.replace(
+ "bg-gray-50 text-gray-400",
+ "bg-gray-100 text-gray-700",
+ );
+ } else {
+ prevBtn.disabled = true;
+ if (!prevBtn.className.includes("opacity-50")) {
+ prevBtn.className += " opacity-50 cursor-not-allowed";
+ }
+ prevBtn.className = prevBtn.className.replace(
+ "bg-gray-100 text-gray-700",
+ "bg-gray-50 text-gray-400",
+ );
+ }
+
+ // Enable/disable next page button
+ if (nextPage !== null) {
+ nextBtn.disabled = false;
+ nextBtn.className = nextBtn.className.replace("opacity-50 cursor-not-allowed", "");
+ nextBtn.className = nextBtn.className.replace(
+ "bg-gray-50 text-gray-400",
+ "bg-gray-100 text-gray-700",
+ );
+ } else {
+ nextBtn.disabled = true;
+ if (!nextBtn.className.includes("opacity-50")) {
+ nextBtn.className += " opacity-50 cursor-not-allowed";
+ }
+ nextBtn.className = nextBtn.className.replace(
+ "bg-gray-100 text-gray-700",
+ "bg-gray-50 text-gray-400",
+ );
+ }
+ }
+
+ // Get previous and next page numbers
+ getAdjacentPages() {
+ // Get all page containers of the same type (main or beilage)
+ let containerSelector;
+ if (this.currentIsBeilage) {
+ containerSelector = '.newspaper-page-container[data-beilage="true"]';
+ } else {
+ containerSelector = ".newspaper-page-container:not([data-beilage])";
+ }
+
+ const pageContainers = Array.from(document.querySelectorAll(containerSelector));
+ console.log(
+ "Found containers:",
+ pageContainers.length,
+ "for",
+ this.currentIsBeilage ? "beilage" : "main",
+ );
+
+ // Extract page numbers and sort them
+ const allPages = pageContainers
+ .map((container) => {
+ const pageAttr = container.getAttribute("data-page-container");
+ const pageNum = pageAttr ? parseInt(pageAttr) : null;
+ console.log("Container page:", pageAttr, "parsed:", pageNum);
+ return pageNum;
+ })
+ .filter((p) => p !== null)
+ .sort((a, b) => a - b);
+
+ console.log("All pages found:", allPages);
+ console.log("Current page:", this.currentPageNumber);
+
+ const currentIndex = allPages.indexOf(this.currentPageNumber);
+ console.log("Current index:", currentIndex);
+
+ let prevPage = null;
+ let nextPage = null;
+
+ if (currentIndex > 0) {
+ prevPage = allPages[currentIndex - 1];
+ }
+
+ if (currentIndex < allPages.length - 1) {
+ nextPage = allPages[currentIndex + 1];
+ }
+
+ console.log("Adjacent pages - prev:", prevPage, "next:", nextPage);
+ return { prevPage, nextPage };
+ }
+
+ // Navigate to previous page
+ goToPreviousPage() {
+ const { prevPage } = this.getAdjacentPages();
+ if (prevPage !== null) {
+ this.navigateToPage(prevPage);
+ }
+ }
+
+ // Navigate to next page
+ goToNextPage() {
+ const { nextPage } = this.getAdjacentPages();
+ if (nextPage !== null) {
+ this.navigateToPage(nextPage);
+ }
+ }
+
+ // Navigate to a specific page
+ navigateToPage(pageNumber) {
+ // Find the image element for the target page
+ const containerSelector = this.currentIsBeilage
+ ? '.newspaper-page-container[data-beilage="true"]'
+ : ".newspaper-page-container:not([data-beilage])";
+ const targetContainer = document.querySelector(
+ `${containerSelector}[data-page-container="${pageNumber}"]`,
+ );
+
+ if (targetContainer) {
+ const imgElement = targetContainer.querySelector(".newspaper-page-image");
+ if (imgElement) {
+ // Determine part number for piece view
+ let newPartNumber = null;
+ if (this.currentPartNumber !== null) {
+ // We're in piece view, try to find the part number for the new page
+ newPartNumber = this.getPartNumberForPage(pageNumber);
+ }
+
+ // Update the current view with the new page
+ this.show(
+ imgElement.src,
+ imgElement.alt,
+ pageNumber,
+ this.currentIsBeilage,
+ 0,
+ newPartNumber,
+ );
+ }
+ }
+ }
+
+ // Get part number for a specific page in piece view
+ getPartNumberForPage(pageNumber) {
+ // Try to find the part number from the page container in piece view
+ const pageContainer = document.querySelector(`[data-page-container="${pageNumber}"]`);
+ if (pageContainer) {
+ const partNumberElement = pageContainer.querySelector(".part-number");
+ if (partNumberElement) {
+ // Extract just the number from "X. Teil" format
+ const match = partNumberElement.textContent.match(/(\d+)\./);
+ if (match) {
+ return parseInt(match[1]);
+ }
+ }
+ }
+
+ // Fallback: if we can't find it, return null to show icon instead
+ return null;
+ }
+
+ // Toggle sidebar visibility
+ toggleSidebar() {
+ const sidebarSpacer = this.querySelector("#sidebar-spacer");
+ const toggleBtn = this.querySelector("#sidebar-toggle-btn");
+ const toggleIcon = toggleBtn.querySelector("i");
+
+ // Check if sidebar is currently collapsed by looking at width
+ const currentWidth = sidebarSpacer.style.width;
+ const isCollapsed = currentWidth === "0px" || currentWidth === "0";
+
+ console.log("Current state - isCollapsed:", isCollapsed);
+ console.log("Current width:", currentWidth);
+
+ if (isCollapsed) {
+ // Sidebar is collapsed, expand it
+ const fullWidth = this.detectSidebarWidth();
+ sidebarSpacer.style.width = fullWidth;
+
+ // Update button to normal state (sidebar visible)
+ toggleBtn.className =
+ "w-10 h-10 bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer";
+ toggleIcon.className = "ri-sidebar-fold-line text-lg font-bold";
+ toggleBtn.title = "Inhaltsverzeichnis ausblenden";
+ console.log("Expanding sidebar to:", fullWidth);
+ } else {
+ // Sidebar is expanded, collapse it
+ sidebarSpacer.style.width = "0px";
+
+ // Update button to active state (sidebar hidden - orange highlight)
+ toggleBtn.className =
+ "w-10 h-10 bg-orange-100 hover:bg-orange-200 text-orange-700 border border-orange-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer";
+ toggleIcon.className = "ri-sidebar-unfold-line text-lg font-bold";
+ toggleBtn.title = "Inhaltsverzeichnis einblenden";
+ console.log("Collapsing sidebar");
+ }
+
+ console.log("New width:", sidebarSpacer.style.width);
+ }
+
+ // Extract issue context from document title, URL, or page container
+ getIssueContext(pageNumber) {
+ // Determine if we're in a piece view (beitrag) or issue view (ausgabe)
+ const path = window.location.pathname;
+ const isPieceView = path.includes("/beitrag/");
+
+ if (isPieceView) {
+ // For piece view: Return full format "1765 Nr. 2"
+ // Try to get context from page container first (for piece view)
+ const pageContainer = document.querySelector(`[data-page-container="${pageNumber}"]`);
+ if (pageContainer) {
+ // Look for existing page indicator with context
+ const pageIndicator = pageContainer.querySelector(".page-indicator");
+ if (pageIndicator) {
+ const text = pageIndicator.textContent.trim();
+ // Extract full date and issue from text like "3.7.1767 Nr. 53, 213"
+ const fullDateMatch = text.match(/(\d{1,2}\.\d{1,2}\.\d{4}\s+Nr\.\s+\d+)/);
+ if (fullDateMatch) {
+ return fullDateMatch[1];
+ }
+ // Fallback: Extract year and issue from text like "1768 Nr. 20, 79"
+ const yearMatch = text.match(/(\d{4})\s+Nr\.\s+(\d+)/);
+ if (yearMatch) {
+ return `${yearMatch[1]} Nr. ${yearMatch[2]}`;
+ }
+ }
+ }
+
+ // Fallback: Try to extract from document title
+ const title = document.title;
+ const titleMatch = title.match(/(\d{4}).*Nr\.\s*(\d+)/);
+ if (titleMatch) {
+ return `${titleMatch[1]} Nr. ${titleMatch[2]}`;
+ }
+ } else {
+ // For issue view: Return just empty string (page number only)
+ return "";
+ }
+
+ // Final fallback: Try to extract from URL path
+ const urlMatch = path.match(/\/(\d{4})\/(\d+)/);
+ if (urlMatch) {
+ return isPieceView ? `${urlMatch[1]} Nr. ${urlMatch[2]}` : "";
+ }
+
+ // Fallback - try to get from any visible page context
+ const anyPageIndicator = document.querySelector(".page-indicator");
+ if (anyPageIndicator) {
+ const text = anyPageIndicator.textContent.trim();
+ const match = text.match(/(\d{4})\s+Nr\.\s+(\d+)/);
+ if (match) {
+ return `${match[1]} Nr. ${match[2]}`;
+ }
+ }
+
+ // Ultimate fallback
+ return "KGPZ";
+ }
+}
+
+// Register the web component
+customElements.define("single-page-viewer", SinglePageViewer);
+
+// Clean up single page viewer on HTMX navigation
+document.body.addEventListener("htmx:beforeRequest", function (event) {
+ // Find any active single page viewer
+ const viewer = document.querySelector("single-page-viewer");
+ if (viewer && viewer.style.display !== "none") {
+ console.log("Cleaning up single page viewer before HTMX navigation");
+ viewer.destroy();
+ }
+});
+
+// Also clean up on page unload as fallback
+window.addEventListener("beforeunload", function () {
+ const viewer = document.querySelector("single-page-viewer");
+ if (viewer) {
+ viewer.destroy();
+ }
+});
\ No newline at end of file
diff --git a/views/transform/site.css b/views/transform/site.css
index 3bea248..179bbe9 100644
--- a/views/transform/site.css
+++ b/views/transform/site.css
@@ -5,6 +5,9 @@
--font-sans: "Source Sans 3", "Merriweather Sans", ui-sans-serif;
--font-serif: "Merriweather", ui-serif;
+ /* Custom breakpoint for 4K+ screens */
+ --breakpoint-3xl: 120.0625rem; /* 1921px */
+
/* Custom Black Colors */
--color-abyss: #0b1215;
--color-shadow: #0f171f;