diff --git a/views/transform/issue.js b/views/transform/issue.js
index b5677d1..cab0d1f 100644
--- a/views/transform/issue.js
+++ b/views/transform/issue.js
@@ -23,8 +23,38 @@ export function enlargePage(imgElement, pageNumber, isFromSpread, partNumber = n
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);
+ // Extract existing icon type and heading from the page container
+ const pageContainer = imgElement.closest('.newspaper-page-container, .piece-page-container');
+ let extractedIconType = null;
+ let extractedHeading = null;
+
+ if (pageContainer) {
+ // Extract icon type from data attribute
+ extractedIconType = pageContainer.getAttribute('data-page-icon-type');
+
+ // For piece view: check if part number should override icon
+ const partNumberElement = pageContainer.querySelector('.part-number');
+ if (partNumberElement) {
+ extractedIconType = 'part-number';
+ }
+
+ // Extract heading text from page indicator
+ const pageIndicator = pageContainer.querySelector('.page-indicator');
+ if (pageIndicator) {
+ // Clone the page indicator to extract text without buttons/icons
+ const indicatorClone = pageIndicator.cloneNode(true);
+ // Remove any icons to get just the text
+ const icons = indicatorClone.querySelectorAll('i');
+ icons.forEach(icon => icon.remove());
+ // Remove any link indicators
+ const linkIndicators = indicatorClone.querySelectorAll('[class*="target-page-dot"], .target-page-indicator');
+ linkIndicators.forEach(indicator => indicator.remove());
+ extractedHeading = indicatorClone.textContent.trim();
+ }
+ }
+
+ // Show the page in the viewer with extracted data
+ viewer.show(imgElement.src, imgElement.alt, pageNumber, isBeilage, targetPage, partNumber, extractedIconType, extractedHeading);
}
export function closeModal() {
@@ -124,7 +154,6 @@ export function scrollToPreviousPage() {
if (targetIndex >= 0) {
window.currentActiveIndex = targetIndex;
window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
- behavior: "smooth",
block: "start",
});
@@ -176,7 +205,6 @@ export function scrollToNextPage() {
if (targetIndex >= 0 && targetIndex < window.currentPageContainers.length) {
window.currentActiveIndex = targetIndex;
window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
- behavior: "smooth",
block: "start",
});
@@ -197,7 +225,6 @@ export function scrollToBeilage() {
const firstMainPage = document.querySelector("#newspaper-content .newspaper-page-container");
if (firstMainPage) {
firstMainPage.scrollIntoView({
- behavior: "smooth",
block: "start",
});
}
@@ -206,7 +233,6 @@ export function scrollToBeilage() {
const beilageContainer = document.querySelector('[class*="border-t-2 border-amber-200"]');
if (beilageContainer) {
beilageContainer.scrollIntoView({
- behavior: "smooth",
block: "start",
});
}
@@ -249,18 +275,30 @@ export function updateButtonStates() {
const beilageBtn = document.getElementById("beilageBtn");
if (prevBtn) {
+ // Always show button, but disable when at first page
+ prevBtn.style.display = "flex";
if (window.currentActiveIndex <= 0) {
- prevBtn.style.display = "none";
+ prevBtn.disabled = true;
+ prevBtn.classList.add("opacity-50", "cursor-not-allowed");
+ prevBtn.classList.remove("hover:bg-gray-200");
} else {
- prevBtn.style.display = "flex";
+ prevBtn.disabled = false;
+ prevBtn.classList.remove("opacity-50", "cursor-not-allowed");
+ prevBtn.classList.add("hover:bg-gray-200");
}
}
if (nextBtn) {
+ // Always show button, but disable when at last page
+ nextBtn.style.display = "flex";
if (window.currentActiveIndex >= window.currentPageContainers.length - 1) {
- nextBtn.style.display = "none";
+ nextBtn.disabled = true;
+ nextBtn.classList.add("opacity-50", "cursor-not-allowed");
+ nextBtn.classList.remove("hover:bg-gray-200");
} else {
- nextBtn.style.display = "flex";
+ nextBtn.disabled = false;
+ nextBtn.classList.remove("opacity-50", "cursor-not-allowed");
+ nextBtn.classList.add("hover:bg-gray-200");
}
}
diff --git a/views/transform/single-page-viewer.js b/views/transform/single-page-viewer.js
index e2045ab..0f4360e 100644
--- a/views/transform/single-page-viewer.js
+++ b/views/transform/single-page-viewer.js
@@ -126,7 +126,7 @@ export class SinglePageViewer extends HTMLElement {
id="single-page-image"
src=""
alt=""
- class="w-full h-auto rounded-lg shadow-2xl cursor-pointer"
+ class="w-full h-auto rounded-lg shadow-2xl cursor-zoom-out"
onclick="this.closest('single-page-viewer').close()"
title="Klicken zum Schließen"
/>
@@ -138,6 +138,9 @@ export class SinglePageViewer extends HTMLElement {
// Set up resize observer to handle window resizing
this.setupResizeObserver();
+
+ // Set up keyboard navigation
+ this.setupKeyboardNavigation();
}
// Set up resize observer to dynamically update sidebar width
@@ -167,7 +170,16 @@ export class SinglePageViewer extends HTMLElement {
}
}
- show(imgSrc, imgAlt, pageNumber, isBeilage = false, targetPage = 0, partNumber = null) {
+ show(
+ imgSrc,
+ imgAlt,
+ pageNumber,
+ isBeilage = false,
+ targetPage = 0,
+ partNumber = null,
+ extractedIconType = null,
+ extractedHeading = null,
+ ) {
const img = this.querySelector("#single-page-image");
const pageNumberSpan = this.querySelector("#page-number");
const pageIconSpan = this.querySelector("#page-icon");
@@ -181,11 +193,18 @@ export class SinglePageViewer extends HTMLElement {
this.currentIsBeilage = isBeilage;
this.currentPartNumber = partNumber;
- // Get issue context from document title or URL
- const issueContext = this.getIssueContext(pageNumber);
+ // Use extracted heading or fallback to generated heading
+ let headingText;
+ if (extractedHeading) {
+ headingText = extractedHeading;
+ } else {
+ // Fallback: generate heading text
+ const issueContext = this.getIssueContext(pageNumber);
+ headingText = issueContext ? `${issueContext}, ${pageNumber}` : `${pageNumber}`;
+ }
- // Set page number with issue context in the box
- pageNumberSpan.innerHTML = issueContext ? `${issueContext}, ${pageNumber}` : `${pageNumber}`;
+ // Set page number with heading text in the box
+ pageNumberSpan.innerHTML = headingText;
// Add red dot if this is the target page
if (targetPage && pageNumber === targetPage) {
@@ -203,14 +222,18 @@ export class SinglePageViewer extends HTMLElement {
pageNumberSpan.appendChild(redDot);
}
- // Set page icon or part number based on view type
- if (partNumber !== null) {
- // Piece view: Show part number instead of icon
- pageIconSpan.innerHTML = `${partNumber}. Teil`;
+ // Use extracted icon type or fallback to generated icon
+ if (extractedIconType) {
+ if (extractedIconType === "part-number" && partNumber !== null) {
+ // Piece view: Show part number instead of icon
+ pageIconSpan.innerHTML = `${partNumber}. Teil`;
+ } else {
+ // Use icon type from Go templates
+ pageIconSpan.innerHTML = this.generateIconFromType(extractedIconType);
+ }
} else {
- // Issue view: Show icon based on position and type
- const iconType = this.determinePageIconType(pageNumber, isBeilage);
- pageIconSpan.innerHTML = this.getPageIconHTML(iconType);
+ // Fallback: generate simple icon
+ pageIconSpan.innerHTML = this.generateFallbackIcon(pageNumber, isBeilage, partNumber);
}
// Page indicator styling is now consistent (white background)
@@ -220,6 +243,12 @@ export class SinglePageViewer extends HTMLElement {
this.style.display = "block";
+ // Scroll to top of the single page viewer (no smooth scrolling)
+ const scrollContainer = this.querySelector(".flex-1.overflow-auto");
+ if (scrollContainer) {
+ scrollContainer.scrollTop = 0;
+ }
+
// Prevent background scrolling but allow scrolling within the viewer
document.body.style.overflow = "hidden";
}
@@ -238,72 +267,64 @@ export class SinglePageViewer extends HTMLElement {
this.resizeObserver = null;
}
+ // Clean up keyboard event listener
+ if (this.keyboardHandler) {
+ document.removeEventListener('keydown', this.keyboardHandler);
+ this.keyboardHandler = 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 icon HTML from Go icon type - matches templating/engine.go PageIcon function
+ generateIconFromType(iconType) {
+ switch (iconType) {
+ case "first":
+ return ``;
+ case "last":
+ return ``;
+ case "even":
+ return ``;
+ case "odd":
+ return ``;
+ case "single":
+ return ``;
+ default:
+ return ``;
}
}
- // Generate page icon HTML based on type (same as Go PageIcon function)
- getPageIconHTML(iconType) {
- const baseClass = "ri-file-text-line text-lg";
-
- switch (iconType) {
- case "first":
- return ``;
- case "last":
- return ``;
- case "even":
- return ``;
- case "odd":
- return ``;
- default:
- return ``;
+ // Set up keyboard navigation
+ setupKeyboardNavigation() {
+ // Remove any existing listener to avoid duplicates
+ if (this.keyboardHandler) {
+ document.removeEventListener('keydown', this.keyboardHandler);
}
+
+ // Create bound handler
+ this.keyboardHandler = (event) => {
+ // Only handle keyboard events when the viewer is visible
+ if (this.style.display === 'none') return;
+
+ switch (event.key) {
+ case 'ArrowLeft':
+ event.preventDefault();
+ this.goToPreviousPage();
+ break;
+ case 'ArrowRight':
+ event.preventDefault();
+ this.goToNextPage();
+ break;
+ case 'Escape':
+ event.preventDefault();
+ this.close();
+ break;
+ }
+ };
+
+ // Add event listener
+ document.addEventListener('keydown', this.keyboardHandler);
}
// Share current page
@@ -447,7 +468,7 @@ export class SinglePageViewer extends HTMLElement {
);
if (targetContainer) {
- const imgElement = targetContainer.querySelector(".newspaper-page-image");
+ const imgElement = targetContainer.querySelector(".newspaper-page-image, .piece-page-image");
if (imgElement) {
// Determine part number for piece view
let newPartNumber = null;
@@ -456,6 +477,35 @@ export class SinglePageViewer extends HTMLElement {
newPartNumber = this.getPartNumberForPage(pageNumber);
}
+ // Extract icon type and heading for the new page
+ let extractedIconType = null;
+ let extractedHeading = null;
+
+ // Extract icon type from data attribute
+ extractedIconType = targetContainer.getAttribute("data-page-icon-type");
+
+ // For piece view: check if part number should override icon
+ const partNumberElement = targetContainer.querySelector(".part-number");
+ if (partNumberElement) {
+ extractedIconType = "part-number";
+ }
+
+ // Extract heading text from page indicator
+ const pageIndicator = targetContainer.querySelector(".page-indicator");
+ if (pageIndicator) {
+ // Clone the page indicator to extract text without buttons/icons
+ const indicatorClone = pageIndicator.cloneNode(true);
+ // Remove any icons to get just the text
+ const icons = indicatorClone.querySelectorAll("i");
+ icons.forEach((icon) => icon.remove());
+ // Remove any link indicators
+ const linkIndicators = indicatorClone.querySelectorAll(
+ '[class*="target-page-dot"], .target-page-indicator',
+ );
+ linkIndicators.forEach((indicator) => indicator.remove());
+ extractedHeading = indicatorClone.textContent.trim();
+ }
+
// Update the current view with the new page
this.show(
imgElement.src,
@@ -464,6 +514,8 @@ export class SinglePageViewer extends HTMLElement {
this.currentIsBeilage,
0,
newPartNumber,
+ extractedIconType,
+ extractedHeading,
);
}
}
@@ -488,6 +540,19 @@ export class SinglePageViewer extends HTMLElement {
return null;
}
+ // Legacy fallback icon generation (only used when extraction fails)
+ generateFallbackIcon(pageNumber, isBeilage, partNumber) {
+ if (partNumber !== null) {
+ // Piece view: Show part number instead of icon
+ return `${partNumber}. Teil`;
+ } else {
+ // Issue view: Simple fallback icon
+ const baseClass = "ri-file-text-line text-lg";
+ const iconColor = isBeilage ? "text-amber-600" : "text-black";
+ return ``;
+ }
+ }
+
// Toggle sidebar visibility
toggleSidebar() {
const sidebarSpacer = this.querySelector("#sidebar-spacer");
@@ -596,7 +661,7 @@ document.body.addEventListener("htmx:beforeRequest", function (event) {
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();
+ viewer.close();
}
});
@@ -604,6 +669,6 @@ document.body.addEventListener("htmx:beforeRequest", function (event) {
window.addEventListener("beforeunload", function () {
const viewer = document.querySelector("single-page-viewer");
if (viewer) {
- viewer.destroy();
+ viewer.close();
}
-});
\ No newline at end of file
+});