Viewer & Icon enhancements

This commit is contained in:
Simon Martens
2025-09-23 01:44:47 +02:00
parent b579539e66
commit fc5e8ae8a4
12 changed files with 497 additions and 281 deletions

View File

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

View File

@@ -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 = `<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>`;
// 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 = `<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 {
// 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 `<i class="ri-file-text-line text-black text-lg" display: inline-block;"></i>`;
case "last":
return `<i class="ri-file-text-line text-black text-lg" style="transform: scaleX(-1); display: inline-block;"></i>`;
case "even":
return `<i class="ri-file-text-line text-black text-lg" style="margin-left: 1px; transform: scaleX(-1); display: inline-block;"></i><i class="ri-file-text-line text-slate-400 text-lg"></i>`;
case "odd":
return `<i class="ri-file-text-line text-slate-400 text-lg" style="margin-left: 1px; transform: scaleX(-1); display: inline-block;"></i><i class="ri-file-text-line text-black text-lg"></i>`;
case "single":
return `<i class="ri-file-text-line text-black text-lg"></i>`;
default:
return `<i class="ri-file-text-line text-black text-lg"></i>`;
}
}
// 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>`;
// 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 `<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: Simple fallback icon
const baseClass = "ri-file-text-line text-lg";
const iconColor = isBeilage ? "text-amber-600" : "text-black";
return `<i class="${baseClass} ${iconColor}"></i>`;
}
}
// 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();
}
});
});