Zoom mode

This commit is contained in:
Simon Martens
2025-09-23 05:12:42 +02:00
parent db7844ed6f
commit fcbd9f9e5c
3 changed files with 685 additions and 285 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,16 @@ export class SinglePageViewer extends HTMLElement {
super();
// No shadow DOM - use regular DOM to allow Tailwind CSS
this.resizeObserver = null;
// Zoom state management
this.zoomLevel = 1.0; // 100% zoom
this.minZoom = 1.0; // Cannot zoom below 100%
this.maxZoom = 4.0; // Maximum 400% zoom
this.panX = 0; // Pan offset X
this.panY = 0; // Pan offset Y
this.isDragging = false;
this.lastMouseX = 0;
this.lastMouseY = 0;
}
// Dynamically detect sidebar width in pixels
@@ -92,6 +102,23 @@ export class SinglePageViewer extends HTMLElement {
<!-- Separator -->
<div class="w-px h-6 bg-gray-300"></div>
<!-- Zoom level indicator -->
<div id="zoom-level-display" class="bg-purple-50 border border-purple-200 rounded px-3 py-2 text-purple-700 text-sm font-medium whitespace-nowrap">
Strg + Mausrad oder +/- für Zoom
</div>
<!-- Reset zoom button -->
<button
id="zoom-reset-btn"
onclick="this.closest('single-page-viewer').resetZoom()"
class="w-10 h-10 bg-purple-100 hover:bg-purple-200 text-purple-700 border border-purple-300 rounded flex items-center justify-center transition-colors duration-200 cursor-pointer"
title="Zoom zurücksetzen (100%)">
<i class="ri-fullscreen-exit-line text-lg font-bold"></i>
</button>
<!-- Separator -->
<div class="w-px h-6 bg-gray-300"></div>
<!-- Share button -->
<button
id="share-btn"
@@ -121,15 +148,18 @@ export class SinglePageViewer extends HTMLElement {
</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-zoom-out"
onclick="this.closest('single-page-viewer').close()"
title="Klicken zum Schließen"
/>
<div id="image-scroll-container" class="flex-1 flex items-center justify-center p-4 pb-8 overflow-auto relative">
<div id="image-container" class="relative">
<img
id="single-page-image"
src=""
alt=""
class="rounded-lg shadow-2xl cursor-zoom-in select-none"
style="transform: scale(1) translate3d(0px, 0px, 0); transform-origin: center center; will-change: transform;"
title="Zoom mit Strg + Mausrad oder +/- Tasten"
draggable="false"
/>
</div>
</div>
</div>
</div>
@@ -141,6 +171,9 @@ export class SinglePageViewer extends HTMLElement {
// Set up keyboard navigation
this.setupKeyboardNavigation();
// Set up zoom functionality
this.setupZoomEvents();
}
// Set up resize observer to dynamically update sidebar width
@@ -244,6 +277,9 @@ export class SinglePageViewer extends HTMLElement {
this.style.display = "block";
this.setAttribute("active", "true");
// Reset zoom state for new image
this.resetZoom();
// Scroll to top of the single page viewer (no smooth scrolling)
const scrollContainer = this.querySelector(".flex-1.overflow-auto");
if (scrollContainer) {
@@ -279,16 +315,53 @@ export class SinglePageViewer extends HTMLElement {
this.resizeObserver = null;
}
// Clean up keyboard event listener
// Clean up keyboard event listeners
if (this.keyboardHandler) {
document.removeEventListener('keydown', this.keyboardHandler);
this.keyboardHandler = null;
}
// Clean up zoom event listeners
this.cleanupZoomEvents();
// Restore background scrolling
document.body.style.overflow = "";
}
// Clean up zoom event listeners
cleanupZoomEvents() {
const container = this.querySelector('#image-scroll-container');
const img = this.querySelector('#single-page-image');
if (this.wheelHandler && container) {
container.removeEventListener('wheel', this.wheelHandler);
this.wheelHandler = null;
}
if (this.mouseDownHandler && img) {
img.removeEventListener('mousedown', this.mouseDownHandler);
this.mouseDownHandler = null;
}
if (this.mouseMoveHandler) {
document.removeEventListener('mousemove', this.mouseMoveHandler);
this.mouseMoveHandler = null;
}
if (this.mouseUpHandler) {
document.removeEventListener('mouseup', this.mouseUpHandler);
this.mouseUpHandler = null;
}
// Cancel any pending animation frames
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
// Generate icon HTML from Go icon type - matches templating/engine.go PageIcon function
generateIconFromType(iconType) {
switch (iconType) {
@@ -339,6 +412,232 @@ export class SinglePageViewer extends HTMLElement {
document.addEventListener('keydown', this.keyboardHandler);
}
// Set up zoom event listeners
setupZoomEvents() {
const img = this.querySelector('#single-page-image');
const container = this.querySelector('#image-scroll-container');
// Mouse wheel zoom with Ctrl key
this.wheelHandler = (event) => {
// Only handle wheel events when viewer is visible and Ctrl is pressed
if (this.style.display === 'none' || !event.ctrlKey) return;
event.preventDefault();
// Calculate zoom direction
const zoomDirection = event.deltaY > 0 ? -1 : 1;
const zoomFactor = 0.1;
// Get mouse position relative to image
const rect = img.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
this.zoom(zoomDirection * zoomFactor, mouseX, mouseY);
};
// Update existing keyboard handler to include zoom
const originalKeyboardHandler = this.keyboardHandler;
this.keyboardHandler = (event) => {
// Only handle keyboard events when the viewer is visible
if (this.style.display === 'none') return;
// Handle zoom keys first
if (event.key === '+' || event.key === '=') {
event.preventDefault();
this.zoom(0.1); // Zoom in by 10%
return;
} else if (event.key === '-') {
event.preventDefault();
this.zoom(-0.1); // Zoom out by 10%
return;
}
// Handle original navigation keys
switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
this.goToPreviousPage();
break;
case 'ArrowRight':
event.preventDefault();
this.goToNextPage();
break;
case 'Escape':
event.preventDefault();
this.close();
break;
}
};
// Mouse down for pan start
this.mouseDownHandler = (event) => {
if (this.style.display === 'none' || this.zoomLevel <= 1.0) return;
// Only start dragging on left mouse button
if (event.button !== 0) return;
event.preventDefault();
this.isDragging = true;
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
// Update cursor
this.updateCursor();
};
// Mouse move for panning
this.mouseMoveHandler = (event) => {
if (this.style.display === 'none') return;
if (this.isDragging && this.zoomLevel > 1.0) {
event.preventDefault();
const deltaX = event.clientX - this.lastMouseX;
const deltaY = event.clientY - this.lastMouseY;
this.panX += deltaX;
this.panY += deltaY;
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
// Use requestAnimationFrame for smooth updates
if (!this.animationFrameId) {
this.animationFrameId = requestAnimationFrame(() => {
this.updateImageTransform();
this.animationFrameId = null;
});
}
}
};
// Mouse up for pan end
this.mouseUpHandler = (event) => {
if (this.isDragging) {
this.isDragging = false;
this.updateCursor();
}
};
// Add event listeners
container.addEventListener('wheel', this.wheelHandler, { passive: false });
img.addEventListener('mousedown', this.mouseDownHandler);
document.addEventListener('mousemove', this.mouseMoveHandler);
document.addEventListener('mouseup', this.mouseUpHandler);
// Prevent context menu on image
img.addEventListener('contextmenu', (e) => e.preventDefault());
}
// Zoom function
zoom(deltaZoom, mouseX = null, mouseY = null) {
const newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.zoomLevel + deltaZoom));
if (newZoom === this.zoomLevel) return; // No change
const img = this.querySelector('#single-page-image');
const rect = img.getBoundingClientRect();
// If mouse position provided, zoom towards that point
if (mouseX !== null && mouseY !== null) {
// Calculate zoom point relative to image center
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Offset from center
const offsetX = mouseX - centerX;
const offsetY = mouseY - centerY;
// Calculate new pan position to keep mouse point stationary
const zoomRatio = newZoom / this.zoomLevel;
this.panX = this.panX * zoomRatio - offsetX * (zoomRatio - 1);
this.panY = this.panY * zoomRatio - offsetY * (zoomRatio - 1);
} else {
// Zoom from center - adjust pan to keep image centered
const zoomRatio = newZoom / this.zoomLevel;
this.panX *= zoomRatio;
this.panY *= zoomRatio;
}
this.zoomLevel = newZoom;
// Reset pan when back to 100%
if (this.zoomLevel <= 1.0) {
this.panX = 0;
this.panY = 0;
}
this.updateImageTransform();
this.updateCursor();
this.updateZoomDisplay();
}
// Update image transform based on zoom and pan
updateImageTransform() {
const img = this.querySelector('#single-page-image');
// Use translate3d for hardware acceleration and better performance
img.style.transform = `scale(${this.zoomLevel}) translate3d(${this.panX / this.zoomLevel}px, ${this.panY / this.zoomLevel}px, 0)`;
}
// Update cursor based on zoom state
updateCursor() {
const img = this.querySelector('#single-page-image');
if (this.isDragging) {
img.style.cursor = 'grabbing';
} else if (this.zoomLevel > 1.0) {
if (this.zoomLevel >= this.maxZoom) {
img.style.cursor = 'zoom-out';
} else {
img.style.cursor = 'grab';
}
} else {
// At 100% zoom
img.style.cursor = 'zoom-in';
}
}
// Update zoom level display and reset button
updateZoomDisplay() {
const zoomDisplay = this.querySelector('#zoom-level-display');
const zoomResetBtn = this.querySelector('#zoom-reset-btn');
if (!zoomDisplay) return;
if (this.zoomLevel <= this.minZoom) {
// At 100% zoom - show instructions
zoomDisplay.textContent = 'Strg + Mausrad oder +/- für Zoom';
// Hide reset button at 100%
if (zoomResetBtn) {
zoomResetBtn.style.display = 'none';
}
} else {
// Above 100% zoom - show percentage
zoomDisplay.textContent = `${Math.round(this.zoomLevel * 100)}%`;
// Show reset button when zoomed
if (zoomResetBtn) {
zoomResetBtn.style.display = 'flex';
}
}
}
// Reset zoom state when showing new image
resetZoom() {
this.zoomLevel = 1.0;
this.panX = 0;
this.panY = 0;
this.isDragging = false;
this.updateImageTransform();
this.updateCursor();
this.updateZoomDisplay();
}
// Share current page
shareCurrentPage() {
if (typeof copyPagePermalink === "function") {