mirror of
				https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
				synced 2025-10-31 18:05:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1035 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1035 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import "./site.css";
 | |
| 
 | |
| const ATTR_XSLT_ONLOAD = "script[xslt-onload]";
 | |
| const ATTR_XSLT_TEMPLATE = "xslt-template";
 | |
| const ATTR_XSLT_STATE = "xslt-transformed";
 | |
| 
 | |
| const xslt_processors = new Map();
 | |
| 
 | |
| 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);
 | |
| 		} else {
 | |
| 			throw new Error("Unknown XSLT template: " + templateId);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	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();
 | |
| 					});
 | |
| 				}
 | |
| 			},
 | |
| 		);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| // ===========================
 | |
| // NEWSPAPER LAYOUT FUNCTIONS
 | |
| // ===========================
 | |
| 
 | |
| // 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",
 | |
| 		},
 | |
| 	);
 | |
| 
 | |
| 	// 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);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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) {
 | |
| 	const modal = document.getElementById("pageModal");
 | |
| 	const modalImage = document.getElementById("modalImage");
 | |
| 
 | |
| 	modalImage.src = imgElement.src;
 | |
| 	modalImage.alt = imgElement.alt;
 | |
| 
 | |
| 	modal.classList.remove("hidden");
 | |
| 
 | |
| 	// Mark current page when enlarged
 | |
| 	markCurrentPageInInhaltsverzeichnis(pageNumber);
 | |
| }
 | |
| 
 | |
| 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 = `#page-${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";
 | |
| 	const currentUrl = window.location.href;
 | |
| 
 | |
| 	// 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: ${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);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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);
 | |
| }
 | |
| 
 | |
| // Hash navigation functions
 | |
| function scrollToPageFromHash() {
 | |
| 	const hash = window.location.hash;
 | |
| 	let pageNumber = "";
 | |
| 	let targetContainer = null;
 | |
| 
 | |
| 	if (hash.startsWith("#page-")) {
 | |
| 		pageNumber = hash.replace("#page-", "");
 | |
| 
 | |
| 		// 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}"]`);
 | |
| 		}
 | |
| 	} else if (hash.startsWith("#beilage-")) {
 | |
| 		// Handle beilage-specific hashes like #beilage-1-page-101
 | |
| 		const match = hash.match(/#beilage-(\d+)-page-(\d+)/);
 | |
| 		if (match) {
 | |
| 			const beilageNum = match[1];
 | |
| 			pageNumber = match[2];
 | |
| 			targetContainer = document.getElementById(`beilage-${beilageNum}-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 pageFragment = "";
 | |
| 	if (isBeilage) {
 | |
| 		pageFragment = `#beilage-1-page-${pageNumber}`;
 | |
| 	} else {
 | |
| 		pageFragment = `#page-${pageNumber}`;
 | |
| 	}
 | |
| 
 | |
| 	const currentUrl = window.location.origin + window.location.pathname + pageFragment;
 | |
| 
 | |
| 	// 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";
 | |
| 	const currentUrl = `${window.location.origin}${window.location.pathname}#page-${pageNumber}`;
 | |
| 
 | |
| 	// 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
 | |
| 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 hash handling
 | |
| 	scrollToPageFromHash();
 | |
| 	window.addEventListener("hashchange", scrollToPageFromHash);
 | |
| 
 | |
| 	// 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;
 | |
| window.scrollToPreviousPage = scrollToPreviousPage;
 | |
| window.scrollToNextPage = scrollToNextPage;
 | |
| window.scrollToBeilage = scrollToBeilage;
 | |
| window.shareCurrentPage = shareCurrentPage;
 | |
| window.generateCitation = generateCitation;
 | |
| window.copyPagePermalink = copyPagePermalink;
 | |
| window.generatePageCitation = generatePageCitation;
 | |
| 
 | |
| // INFO: This is intended to be called once on website load
 | |
| function setup() {
 | |
| 	setup_xslt();
 | |
| 	setup_templates();
 | |
| 
 | |
| 	// Initialize newspaper layout if present
 | |
| 	if (document.querySelector(".newspaper-page-container")) {
 | |
| 		initializeNewspaperLayout();
 | |
| 	}
 | |
| 
 | |
| 	// 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();
 | |
| 	});
 | |
| 
 | |
| 	// HTMX event handling for newspaper layout
 | |
| 	document.body.addEventListener("htmx:afterSwap", function (event) {
 | |
| 		setTimeout(() => {
 | |
| 			if (document.querySelector(".newspaper-page-container")) {
 | |
| 				initializeNewspaperLayout();
 | |
| 			}
 | |
| 		}, 100);
 | |
| 	});
 | |
| 
 | |
| 	document.body.addEventListener("htmx:afterSettle", function (event) {
 | |
| 		setTimeout(() => {
 | |
| 			if (document.querySelector(".newspaper-page-container")) {
 | |
| 				initializeNewspaperLayout();
 | |
| 			}
 | |
| 		}, 200);
 | |
| 	});
 | |
| 
 | |
| 	document.body.addEventListener("htmx:load", function (event) {
 | |
| 		setTimeout(() => {
 | |
| 			if (document.querySelector(".newspaper-page-container")) {
 | |
| 				initializeNewspaperLayout();
 | |
| 			}
 | |
| 		}, 100);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| export { setup };
 | 
