THE GREAT JS CLEANUP

This commit is contained in:
Simon Martens
2025-09-22 17:30:21 +02:00
parent 0307bd9c9c
commit f8d7b92be0
18 changed files with 2351 additions and 2898 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<div class="mt-12">Datenschutz, Impressum.</div>
<!-- <div class="mt-12">Datenschutz, Impressum.</div> -->

View File

@@ -6,7 +6,7 @@
<!-- Three-column layout -->
<div class="flex flex-col lg:flex-row gap-6 w-full min-h-screen">
<!-- Column 1: Sticky Inhaltsverzeichnis -->
<div class="lg:w-1/4 xl:w-1/4 flex-shrink-0 bg-slate-50 px-4 py-4">
<div class="lg:w-1/4 xl:w-1/4 2xl:w-1/4 3xl:w-1/5 flex-shrink-0 bg-slate-50 px-4 py-4">
<div class="sticky top-0 max-h-screen overflow-y-auto overscroll-contain bg-slate-50">
<div class="mb-4">
{{ template "_title_nav" . }}

View File

@@ -1,32 +1 @@
<title>KGPZ &ndash; Ausgabe {{ .model.Number.No }}&hairsp;/&hairsp;{{ .model.Year }}</title>
<script>
// Make template data available to JavaScript
window.templateData = {
targetPage: {{ if .targetPage }}{{ .targetPage }}{{ else }}0{{ end }}
};
</script>
{{ if .targetPage }}
<script>
// Auto-scroll to target page when page loads
document.addEventListener('DOMContentLoaded', function() {
// Wait for newspaper layout to be initialized
setTimeout(function() {
const targetPage = {{ .targetPage }};
const pageContainer = document.querySelector('[data-page-container="' + targetPage + '"]');
if (pageContainer) {
pageContainer.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Highlight the page in the Inhaltsverzeichnis
if (typeof markCurrentPageInInhaltsverzeichnis === 'function') {
markCurrentPageInInhaltsverzeichnis(targetPage);
}
}
}, 500); // Give time for layout initialization
});
</script>
{{ end }}

View File

@@ -48,8 +48,7 @@
{{ " " }}{{- range $groupIndex, $groupItem := $groupedItems -}}
{{- range $issueIndex, $issue := $groupItem.Item.IssueRefs -}}
{{- if or (gt $groupIndex 0) (gt $issueIndex 0) }}, {{ end -}}
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{ template "_citation" $issue }}</span>
{{- end -}}
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{- template "_citation" $issue -}}</span>{{- end -}}
{{- end -}}
{{- /* Add "Ganzer Beitrag" link if piece spans multiple issues */ -}}
{{- $firstGroupItem := index $groupedItems 0 -}}

View File

@@ -182,8 +182,7 @@
{{ " " }}{{- range $pieceIndex, $p := $categoryPieces -}}
{{- range $issueIndex, $issue := $p.Item.IssueRefs -}}
{{- if or (gt $pieceIndex 0) (gt $issueIndex 0) }}, {{ end -}}
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{ template "_citation" $issue }}</span>
{{- end -}}
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{- template "_citation" $issue -}}</span>{{- end -}}
{{- end -}}
{{- /* Add "Ganzer Beitrag" link if piece spans multiple issues */ -}}
{{- $firstPiece := index $categoryPieces 0 -}}
@@ -304,8 +303,7 @@
{{ " " }}{{- range $pieceIndex, $p := $categoryPieces -}}
{{- range $issueIndex, $issue := $p.Item.IssueRefs -}}
{{- if or (gt $pieceIndex 0) (gt $issueIndex 0) }}, {{ end -}}
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{ template "_citation" $issue }}</span>
{{- end -}}
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{- template "_citation" $issue -}}</span>{{- end -}}
{{- end -}}
{{- /* Add "Ganzer Beitrag" link if piece spans multiple issues */ -}}
{{- $firstPiece := index $categoryPieces 0 -}}

View File

@@ -32,56 +32,9 @@
{{ $issue.When.Year }}/{{ $issue.Nr }}
{{- end -}}
{{- if $issue.Von }}, {{ if $issue.Beilage }}Beil. {{ else }}S. {{ end }}{{ $issue.Von }}{{- if and $issue.Bis (ne $issue.Von $issue.Bis) -}}
-{{ $issue.Bis }}
-{{- $issue.Bis -}}
{{- end -}}
{{- end -}}
</a>
<script>
// Check if citation links point to current page (without hash)
function updateCitationLinks() {
const currentPath = window.location.pathname;
const citationLinks = document.querySelectorAll('.citation-link[data-citation-url]');
citationLinks.forEach(link => {
const citationUrl = link.getAttribute('data-citation-url');
let isCurrentPage = false;
// Check for exact match
if (citationUrl === currentPath) {
isCurrentPage = true;
} else {
// 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;
}
}
}
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');
}
});
}
// Run on initial load
document.addEventListener('DOMContentLoaded', updateCitationLinks);
// Run on HTMX page swaps
document.addEventListener('htmx:afterSwap', updateCitationLinks);
document.addEventListener('htmx:afterSettle', updateCitationLinks);
</script>
{{- /* This is a dummy element to remove all whitespace at the end of the file, as some editiors
will insert a newline at the end of the file, UGH. */ -}}

View File

@@ -63,3 +63,6 @@
</div>
</div>
<!-- Akteure Scrollspy Web Component -->
<akteure-scrollspy></akteure-scrollspy>

View File

@@ -6,14 +6,14 @@
<!-- Two-column layout for piece view -->
<div class="flex flex-col lg:flex-row gap-6 w-full min-h-screen">
<!-- Column 1: Table of Contents ONLY -->
<div class="lg:w-1/4 xl:w-1/4 flex-shrink-0 bg-slate-50 px-4 py-4">
<div class="lg:w-1/4 xl:w-1/4 2xl:w-1/4 3xl:w-1/5 flex-shrink-0 bg-slate-50 px-4 py-4">
<div class="sticky top-0 max-h-screen overflow-y-auto overscroll-contain bg-slate-50">
{{ template "_piece_inhaltsverzeichnis" . }}
</div>
</div>
<!-- Column 2: Sequential Page Layout -->
<div class="lg:w-3/4 xl:w-3/4 flex-1 py-4">
<div class="lg:w-3/4 xl:w-3/4 2xl:w-3/4 3xl:w-4/5 flex-1 py-4">
{{ template "_piece_sequential_layout" . }}
</div>
</div>

View File

@@ -1,32 +1 @@
<title>KGPZ &ndash; {{ if .model.Title }}{{ .model.Title }}{{ else }}Beitrag{{ end }} ({{ len .model.IssueContexts }} Teil{{ if gt (len .model.IssueContexts) 1 }}e{{ end }})</title>
<script>
// Make template data available to JavaScript
window.templateData = {
targetPage: {{ if .targetPage }}{{ .targetPage }}{{ else }}0{{ end }}
};
</script>
{{ if .targetPage }}
<script>
// Auto-scroll to target page when page loads
document.addEventListener('DOMContentLoaded', function() {
// Wait for newspaper layout to be initialized
setTimeout(function() {
const targetPage = {{ .targetPage }};
const pageContainer = document.querySelector('[data-page-container="' + targetPage + '"]');
if (pageContainer) {
pageContainer.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Highlight the page in the Inhaltsverzeichnis if function exists
if (typeof markCurrentPageInInhaltsverzeichnis === 'function') {
markCurrentPageInInhaltsverzeichnis(targetPage);
}
}
}, 500); // Give time for layout initialization
});
</script>
{{ end }}
<title>KGPZ &ndash; {{ if .model.Title }}{{ .model.Title }}{{ else }}Beitrag{{ end }} ({{ len .model.IssueContexts }} Teil{{ if gt (len .model.IssueContexts) 1 }}e{{ end }})</title>

405
views/transform/akteure.js Normal file
View File

@@ -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);

595
views/transform/issue.js Normal file
View File

@@ -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();
}
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 = `
<button
id="scroll-to-top-btn"
class="fixed bottom-6 right-6 w-12 h-12 bg-gray-700 hover:bg-gray-800 text-gray-100 rounded-full shadow-lg hover:shadow-xl transition-all duration-300 flex items-center justify-center cursor-pointer z-50 opacity-0 pointer-events-none"
title="Nach oben scrollen"
onclick="this.closest('scroll-to-top-button').scrollToTop()">
<i class="ri-arrow-up-line text-xl font-bold"></i>
</button>
`;
// 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);

View File

@@ -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 = `
<div class="fixed inset-0 z-50 flex pointer-events-none">
<!-- Keep Inhaltsverzeichnis area empty/transparent (collapsible) -->
<div id="sidebar-spacer" style="width: ${sidebarWidth};" class="flex-shrink-0 transition-all duration-300"></div>
<!-- Cover the right columns with the zoomed view -->
<div class="flex-1 bg-slate-50 overflow-auto pointer-events-auto">
<div class="relative min-h-full flex flex-col">
<!-- Header with page info and buttons -->
<div class="flex items-center justify-between p-4">
<!-- Left: Sidebar toggle and page indicator -->
<div class="flex items-center gap-3">
<!-- Sidebar toggle button -->
<button
id="sidebar-toggle-btn"
onclick="this.closest('single-page-viewer').toggleSidebar()"
class="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"
title="Inhaltsverzeichnis ein-/ausblenden">
<i class="ri-sidebar-fold-line text-lg font-bold"></i>
</button>
<!-- Page indicator with icon -->
<div id="page-indicator" class="text-slate-800 flex items-center gap-3">
<span id="page-icon" class="text-lg"></span>
<span id="page-number" class="text-lg font-bold bg-blue-50 px-2 py-1 rounded flex items-center gap-1"></span>
</div>
</div>
<!-- Right: Action buttons -->
<div class="flex items-center gap-3">
<!-- Previous page button -->
<button
id="prev-page-btn"
onclick="this.closest('single-page-viewer').goToPreviousPage()"
class="w-10 h-10 bg-gray-100 hover:bg-gray-200 text-gray-700 border border-gray-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer"
title="Vorherige Seite">
<i class="ri-arrow-left-line text-lg font-bold"></i>
</button>
<!-- Next page button -->
<button
id="next-page-btn"
onclick="this.closest('single-page-viewer').goToNextPage()"
class="w-10 h-10 bg-gray-100 hover:bg-gray-200 text-gray-700 border border-gray-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer"
title="Nächste Seite">
<i class="ri-arrow-right-line text-lg font-bold"></i>
</button>
<!-- Separator -->
<div class="w-px h-6 bg-gray-300"></div>
<!-- Share button -->
<button
id="share-btn"
onclick="this.closest('single-page-viewer').shareCurrentPage()"
class="w-10 h-10 bg-blue-100 hover:bg-blue-200 text-blue-700 border border-blue-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer"
title="Link zu dieser Seite kopieren">
<i class="ri-share-line text-lg font-bold"></i>
</button>
<!-- Cite button -->
<button
id="cite-btn"
onclick="this.closest('single-page-viewer').generatePageCitation()"
class="w-10 h-10 bg-green-100 hover:bg-green-200 text-green-700 border border-green-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer"
title="Zitation für diese Seite generieren">
<i class="ri-file-text-line text-lg font-bold"></i>
</button>
<!-- Close button -->
<button
onclick="this.closest('single-page-viewer').close()"
class="w-10 h-10 bg-gray-100 hover:bg-gray-200 text-gray-700 border border-gray-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer"
title="Schließen">
<i class="ri-close-line text-lg font-bold"></i>
</button>
</div>
</div>
<!-- Image container that can scroll -->
<div class="flex-1 flex items-center justify-center p-4 pb-8">
<img
id="single-page-image"
src=""
alt=""
class="w-full h-auto rounded-lg shadow-2xl cursor-pointer"
onclick="this.closest('single-page-viewer').close()"
title="Klicken zum Schließen"
/>
</div>
</div>
</div>
</div>
`;
// 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 = `<span class="part-number bg-slate-100 text-slate-800 font-bold px-1.5 py-0.5 rounded border border-slate-400 flex items-center justify-center">${partNumber}. Teil</span>`;
} 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 `<i class="${baseClass} text-black"></i>`;
case "last":
return `<i class="${baseClass} text-black" style="transform: scaleX(-1); display: inline-block;"></i>`;
case "even":
return `<i class="${baseClass} text-black" style="margin-left: 2px; transform: scaleX(-1); display: inline-block;"></i><i class="${baseClass} text-slate-400"></i>`;
case "odd":
return `<i class="${baseClass} text-slate-400" style="margin-left: 2px; transform: scaleX(-1); display: inline-block;"></i><i class="${baseClass} text-black"></i>`;
default:
return `<i class="${baseClass} text-black"></i>`;
}
}
// 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();
}
});

View File

@@ -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;