feat: add draggable zoom control with close button and menu toggle
- Add close button (X) to zoom control widget - Make zoom control draggable anywhere on screen - Persist dragged position in localStorage (cv-zoom-position) - Add "Zoom" button to action bar to show control when hidden - Persist visibility state in localStorage (cv-zoom-visible) - Cursor changes to "move" to indicate draggability - Interactive elements (slider, buttons) don't trigger drag - Position stays within viewport bounds - All features work on desktop only (zoom hidden on mobile)
This commit is contained in:
+29
-1
@@ -3474,7 +3474,7 @@ html {
|
|||||||
|
|
||||||
.zoom-control {
|
.zoom-control {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 100px; /* Increased from 70px to clear footer */
|
bottom: 100px; /* Default position, can be dragged */
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
@@ -3490,6 +3490,34 @@ html {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
cursor: move; /* Indicate draggability */
|
||||||
|
user-select: none; /* Prevent text selection while dragging */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close button for zoom control */
|
||||||
|
.zoom-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: rgba(220, 53, 69, 0.9);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-close-btn:hover {
|
||||||
|
background: rgba(220, 53, 69, 1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-control:hover {
|
.zoom-control:hover {
|
||||||
|
|||||||
+145
-1
@@ -405,6 +405,144 @@
|
|||||||
applyZoom(newZoom, true);
|
applyZoom(newZoom, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make zoom control draggable and persist position
|
||||||
|
*/
|
||||||
|
function initZoomDragging() {
|
||||||
|
const zoomControl = document.getElementById('zoom-control');
|
||||||
|
if (!zoomControl || isMobileView()) return;
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
let currentX, currentY, initialX, initialY;
|
||||||
|
|
||||||
|
// Restore saved position from localStorage
|
||||||
|
const savedPosition = localStorage.getItem('cv-zoom-position');
|
||||||
|
if (savedPosition) {
|
||||||
|
const { bottom, left } = JSON.parse(savedPosition);
|
||||||
|
zoomControl.style.bottom = bottom;
|
||||||
|
zoomControl.style.left = left;
|
||||||
|
zoomControl.style.transform = 'none'; // Remove centering transform when positioned
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start drag on mousedown (but not on slider, close button, or reset button)
|
||||||
|
zoomControl.addEventListener('mousedown', function(e) {
|
||||||
|
// Ignore if clicking on interactive elements
|
||||||
|
if (e.target.closest('.zoom-slider, .zoom-close-btn, .zoom-reset-btn')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
zoomControl.style.transition = 'none'; // Disable transitions during drag
|
||||||
|
|
||||||
|
// Get current position
|
||||||
|
const rect = zoomControl.getBoundingClientRect();
|
||||||
|
initialX = e.clientX - rect.left;
|
||||||
|
initialY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag on mousemove
|
||||||
|
document.addEventListener('mousemove', function(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
currentX = e.clientX - initialX;
|
||||||
|
currentY = e.clientY - initialY;
|
||||||
|
|
||||||
|
// Keep within viewport bounds
|
||||||
|
const maxX = window.innerWidth - zoomControl.offsetWidth;
|
||||||
|
const maxY = window.innerHeight - zoomControl.offsetHeight;
|
||||||
|
|
||||||
|
currentX = Math.max(0, Math.min(currentX, maxX));
|
||||||
|
currentY = Math.max(0, Math.min(currentY, maxY));
|
||||||
|
|
||||||
|
// Update position
|
||||||
|
zoomControl.style.left = currentX + 'px';
|
||||||
|
zoomControl.style.bottom = (window.innerHeight - currentY - zoomControl.offsetHeight) + 'px';
|
||||||
|
zoomControl.style.transform = 'none'; // Remove centering transform
|
||||||
|
});
|
||||||
|
|
||||||
|
// End drag on mouseup
|
||||||
|
document.addEventListener('mouseup', function() {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
zoomControl.style.transition = 'all 0.3s ease'; // Re-enable transitions
|
||||||
|
|
||||||
|
// Save position to localStorage
|
||||||
|
const position = {
|
||||||
|
bottom: zoomControl.style.bottom,
|
||||||
|
left: zoomControl.style.left
|
||||||
|
};
|
||||||
|
localStorage.setItem('cv-zoom-position', JSON.stringify(position));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide zoom control and show menu button
|
||||||
|
*/
|
||||||
|
function hideZoomControl() {
|
||||||
|
const zoomControl = document.getElementById('zoom-control');
|
||||||
|
const showButton = document.getElementById('show-zoom-btn');
|
||||||
|
|
||||||
|
if (zoomControl) {
|
||||||
|
zoomControl.style.display = 'none';
|
||||||
|
localStorage.setItem('cv-zoom-visible', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showButton) {
|
||||||
|
showButton.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show zoom control and hide menu button (global function for onclick)
|
||||||
|
*/
|
||||||
|
window.showZoomControl = function() {
|
||||||
|
const zoomControl = document.getElementById('zoom-control');
|
||||||
|
const showButton = document.getElementById('show-zoom-btn');
|
||||||
|
|
||||||
|
if (zoomControl) {
|
||||||
|
zoomControl.style.display = 'flex';
|
||||||
|
localStorage.setItem('cv-zoom-visible', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showButton) {
|
||||||
|
showButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize zoom visibility state from localStorage
|
||||||
|
*/
|
||||||
|
function initZoomVisibility() {
|
||||||
|
if (isMobileView()) return; // Always hidden on mobile
|
||||||
|
|
||||||
|
const zoomControl = document.getElementById('zoom-control');
|
||||||
|
const showButton = document.getElementById('show-zoom-btn');
|
||||||
|
const isVisible = localStorage.getItem('cv-zoom-visible');
|
||||||
|
|
||||||
|
// Default to visible if not set
|
||||||
|
if (isVisible === 'false') {
|
||||||
|
if (zoomControl) zoomControl.style.display = 'none';
|
||||||
|
if (showButton) showButton.style.display = 'inline-flex';
|
||||||
|
} else {
|
||||||
|
if (zoomControl) zoomControl.style.display = 'flex';
|
||||||
|
if (showButton) showButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup close button
|
||||||
|
const closeBtn = document.getElementById('zoom-close');
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation(); // Prevent drag from starting
|
||||||
|
hideZoomControl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// PRINT & PDF
|
// PRINT & PDF
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -518,8 +656,14 @@
|
|||||||
window.toggleTheme();
|
window.toggleTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize zoom control
|
// Initialize zoom control (zoom level, event listeners)
|
||||||
initZoomControl();
|
initZoomControl();
|
||||||
|
|
||||||
|
// Initialize zoom visibility state (show/hide based on localStorage)
|
||||||
|
initZoomVisibility();
|
||||||
|
|
||||||
|
// Initialize zoom dragging (make draggable, restore position)
|
||||||
|
initZoomDragging();
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
+18
-1
@@ -174,6 +174,15 @@
|
|||||||
|
|
||||||
<!-- Right: Action buttons -->
|
<!-- Right: Action buttons -->
|
||||||
<div class="action-buttons-right">
|
<div class="action-buttons-right">
|
||||||
|
<button
|
||||||
|
id="show-zoom-btn"
|
||||||
|
class="action-btn zoom-toggle-btn"
|
||||||
|
onclick="showZoomControl()"
|
||||||
|
style="display: none;"
|
||||||
|
aria-label="{{if eq .Lang "es"}}Mostrar control de zoom{{else}}Show zoom control{{end}}">
|
||||||
|
<iconify-icon icon="mdi:magnify" width="18" height="18"></iconify-icon>
|
||||||
|
{{if eq .Lang "es"}}Zoom{{else}}Zoom{{end}}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="action-btn pdf-btn"
|
class="action-btn pdf-btn"
|
||||||
onclick="openPdfModal()"
|
onclick="openPdfModal()"
|
||||||
@@ -454,8 +463,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Zoom Control (Fixed Bottom Center) -->
|
<!-- Zoom Control (Fixed Bottom Center, Draggable) -->
|
||||||
<div id="zoom-control" class="zoom-control no-print" role="group" aria-label="{{if eq .Lang "es"}}Control de zoom{{else}}Zoom control{{end}}">
|
<div id="zoom-control" class="zoom-control no-print" role="group" aria-label="{{if eq .Lang "es"}}Control de zoom{{else}}Zoom control{{end}}">
|
||||||
|
<button
|
||||||
|
id="zoom-close"
|
||||||
|
class="zoom-close-btn"
|
||||||
|
aria-label="{{if eq .Lang "es"}}Cerrar control de zoom{{else}}Close zoom control{{end}}"
|
||||||
|
title="{{if eq .Lang "es"}}Cerrar{{else}}Close{{end}}">
|
||||||
|
<iconify-icon icon="mdi:close" width="16" height="16"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<span class="zoom-value zoom-value-min" aria-hidden="true">50</span>
|
<span class="zoom-value zoom-value-min" aria-hidden="true">50</span>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user