Reset viewer

This commit is contained in:
Simon Martens
2025-09-23 16:33:38 +02:00
parent 0dd99439a8
commit 10cfc5369e
5 changed files with 190 additions and 644 deletions

View File

@@ -8,21 +8,6 @@ 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;
// Click tracking for close functionality
this.clickStartTime = null;
this.clickStartX = null;
this.clickStartY = null;
}
// Dynamically detect sidebar width in pixels
@@ -107,23 +92,6 @@ 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-zoom-out-line text-lg font-bold"></i>
</button>
<!-- Separator -->
<div class="w-px h-6 bg-gray-300"></div>
<!-- Share button -->
<button
id="share-btn"
@@ -152,19 +120,17 @@ export class SinglePageViewer extends HTMLElement {
</div>
</div>
<!-- Image container that can scroll -->
<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-out select-none"
style="transform: scale(1) translate3d(0px, 0px, 0); transform-origin: center center; will-change: transform;"
title="Klicken zum Schließen oder Zoom mit Strg + Mausrad / +/- Tasten"
draggable="false"
/>
</div>
<!-- Image display container -->
<div id="image-container" class="flex-1 flex items-center justify-center p-4 pb-8 overflow-auto">
<img
id="single-page-image"
src=""
alt=""
class="rounded-lg shadow-2xl cursor-pointer select-none max-w-full max-h-full"
title="Klicken zum Schließen"
draggable="false"
onclick="this.closest('single-page-viewer').close()"
/>
</div>
</div>
</div>
@@ -176,9 +142,6 @@ 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
@@ -282,9 +245,6 @@ 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) {
@@ -326,46 +286,11 @@ export class SinglePageViewer extends HTMLElement {
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) {
@@ -397,17 +322,6 @@ export class SinglePageViewer extends HTMLElement {
// 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;
}
switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
@@ -428,283 +342,6 @@ 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);
};
// Mouse down for pan start or close
this.mouseDownHandler = (event) => {
if (this.style.display === 'none') return;
// Only handle left mouse button
if (event.button !== 0) return;
if (this.zoomLevel <= 1.0) {
// At 100% zoom - prepare for close on click
this.clickStartTime = Date.now();
this.clickStartX = event.clientX;
this.clickStartY = event.clientY;
} else {
// Above 100% zoom - prepare for pan
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 or close
this.mouseUpHandler = (event) => {
if (this.isDragging) {
this.isDragging = false;
this.updateCursor();
} else if (this.zoomLevel <= 1.0 && this.clickStartTime) {
// Check if this was a click (not a drag) at 100% zoom
const clickDuration = Date.now() - this.clickStartTime;
const clickDistance = Math.sqrt(
Math.pow(event.clientX - this.clickStartX, 2) +
Math.pow(event.clientY - this.clickStartY, 2)
);
// If it was a quick click with minimal movement, close the viewer
if (clickDuration < 300 && clickDistance < 10) {
this.close();
}
// Reset click tracking
this.clickStartTime = null;
this.clickStartX = null;
this.clickStartY = null;
}
};
// 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();
this.updateScrollBehavior();
}
// Update image transform based on zoom and pan
updateImageTransform() {
const img = this.querySelector('#single-page-image');
const imageContainer = this.querySelector('#image-container');
// 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)`;
// Make the container grow with the zoom level to provide scrollable area
if (this.zoomLevel > 1.0) {
// Use viewport dimensions as base and scale them up
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Make container much larger than viewport to ensure scrollable area
const containerWidth = viewportWidth * this.zoomLevel * 1.5;
const containerHeight = viewportHeight * this.zoomLevel * 1.5;
imageContainer.style.width = `${containerWidth}px`;
imageContainer.style.height = `${containerHeight}px`;
imageContainer.style.minWidth = `${containerWidth}px`;
imageContainer.style.minHeight = `${containerHeight}px`;
console.log(`Zoom ${this.zoomLevel}: Container size set to ${containerWidth}x${containerHeight}`);
} else {
// Reset container size at 100% zoom
imageContainer.style.width = '';
imageContainer.style.height = '';
imageContainer.style.minWidth = '';
imageContainer.style.minHeight = '';
}
}
// 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 - zoom-out cursor indicates click to close
img.style.cursor = 'zoom-out';
}
}
// 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.innerHTML = 'Strg + Mausrad oder +/- für Zoom';
// Hide reset button at 100%
if (zoomResetBtn) {
zoomResetBtn.style.display = 'none';
}
} else {
// Above 100% zoom - show zoom mode with percentage
zoomDisplay.innerHTML = `<span class="font-bold">Zoom aktiv</span> • ${Math.round(this.zoomLevel * 100)}%`;
// Show reset button when zoomed
if (zoomResetBtn) {
zoomResetBtn.style.display = 'flex';
}
}
}
// Update scroll behavior based on zoom level
updateScrollBehavior() {
const mainContainer = this.querySelector('.flex-1.bg-slate-50.overflow-auto');
const imageScrollContainer = this.querySelector('#image-scroll-container');
if (!mainContainer || !imageScrollContainer) return;
if (this.zoomLevel > 1.0) {
// Disable scrolling on main container and make image container full height
mainContainer.style.overflow = 'hidden';
imageScrollContainer.style.height = '100vh';
// Allow overflow in both directions for panning with scroll wheel/two fingers
imageScrollContainer.style.overflow = 'auto';
// Remove the centering behavior when zoomed to allow full scrolling
imageScrollContainer.style.alignItems = 'flex-start';
imageScrollContainer.style.justifyContent = 'flex-start';
} else {
// Reset to normal scrolling behavior
mainContainer.style.overflow = 'auto';
imageScrollContainer.style.height = '';
imageScrollContainer.style.overflow = 'auto';
// Restore centering at 100% zoom
imageScrollContainer.style.alignItems = 'center';
imageScrollContainer.style.justifyContent = 'center';
}
}
// Reset zoom state when showing new image
resetZoom() {
this.zoomLevel = 1.0;
this.panX = 0;
this.panY = 0;
this.isDragging = false;
// Reset click tracking
this.clickStartTime = null;
this.clickStartX = null;
this.clickStartY = null;
this.updateImageTransform();
this.updateCursor();
this.updateZoomDisplay();
this.updateScrollBehavior();
}
// Share current page
shareCurrentPage() {