240 lines
10 KiB
JavaScript
240 lines
10 KiB
JavaScript
// CV Interactive Features - CSP-Compliant External JavaScript
|
|
// Extracted from inline scripts for security hardening
|
|
(function() {
|
|
'use strict';
|
|
|
|
// =============================================================================
|
|
// NAVIGATION & MENU SYSTEM
|
|
// =============================================================================
|
|
|
|
// Minimal menu control - CSS handles most logic, JS just bridges hamburger to menu
|
|
function initMenuSystem() {
|
|
const hamburgerBtn = document.querySelector('.hamburger-btn');
|
|
const menu = document.getElementById('navigation-menu');
|
|
|
|
if (!hamburgerBtn || !menu) return;
|
|
|
|
// Show menu on hamburger hover - CSS handles the rest
|
|
hamburgerBtn.addEventListener('mouseenter', () => menu.classList.add('menu-hover'));
|
|
|
|
// Hide menu when leaving hamburger if not hovering menu
|
|
hamburgerBtn.addEventListener('mouseleave', () => {
|
|
setTimeout(() => {
|
|
if (!menu.matches(':hover')) menu.classList.remove('menu-hover');
|
|
}, 100);
|
|
});
|
|
|
|
// Hide menu when leaving menu
|
|
menu.addEventListener('mouseleave', () => menu.classList.remove('menu-hover'));
|
|
|
|
// Position submenu dynamically (needed because fixed positioning)
|
|
const submenuTrigger = document.querySelector('.menu-item-submenu');
|
|
const submenuContent = document.querySelector('.submenu-content');
|
|
if (submenuTrigger && submenuContent) {
|
|
submenuTrigger.addEventListener('mouseenter', function() {
|
|
submenuContent.style.top = `${this.getBoundingClientRect().top}px`;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Flag to keep header visible after navigation
|
|
let keepHeaderVisible = false;
|
|
|
|
// Expand/collapse all sections utility functions
|
|
window.expandAllSections = function(event) {
|
|
event.preventDefault();
|
|
document.querySelectorAll('details').forEach(d => d.setAttribute('open', ''));
|
|
};
|
|
|
|
window.collapseAllSections = function(event) {
|
|
event.preventDefault();
|
|
document.querySelectorAll('details').forEach(d => d.removeAttribute('open'));
|
|
};
|
|
|
|
// Close menu when navigation links clicked - CSS handles scrolling
|
|
document.addEventListener('click', (e) => {
|
|
const navLink = e.target.closest('.submenu-content a[href^="#"]');
|
|
if (navLink) {
|
|
document.querySelector('.navigation-menu')?.classList.remove('menu-hover', 'menu-open');
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// LANGUAGE & PREFERENCES
|
|
// =============================================================================
|
|
|
|
// =============================================================================
|
|
// ZOOM CONTROL - Now handled by Hyperscript in zoom-control.html
|
|
// =============================================================================
|
|
|
|
// All zoom functionality moved to declarative hyperscript:
|
|
// - Slider updates and real-time zoom application
|
|
// - Reset button (back to 100%)
|
|
// - Close/show toggle with localStorage persistence
|
|
// - Keyboard shortcuts (Ctrl/Cmd +/-/0)
|
|
// - Draggable positioning with bounds checking
|
|
// - Mobile detection and auto-disable
|
|
// - LocalStorage persistence (zoom level, visibility, position)
|
|
//
|
|
// Result: ~343 lines of JavaScript eliminated!
|
|
|
|
// =============================================================================
|
|
// PRINT & PDF - Now handled by Hyperscript in action-buttons.html
|
|
// =============================================================================
|
|
|
|
// Print function moved to inline hyperscript on print button:
|
|
// - Stores current theme, length, and zoom state
|
|
// - Applies clean theme + short version for printing
|
|
// - Resets zoom to 100% for consistent print output
|
|
// - Calls window.print()
|
|
// - Restores original state after print dialog closes
|
|
// - Properly restores zoom by triggering slider input event (fixes Phase 5 bug)
|
|
//
|
|
// Result: ~44 lines of JavaScript eliminated!
|
|
|
|
// =============================================================================
|
|
// INITIALIZATION & PREFERENCES
|
|
// =============================================================================
|
|
|
|
function initPreferences() {
|
|
const paper = document.querySelector('.cv-paper');
|
|
|
|
// Handle language preference
|
|
const urlLang = new URLSearchParams(window.location.search).get('lang');
|
|
const savedLang = localStorage.getItem('cv-language');
|
|
|
|
if (!urlLang && savedLang) {
|
|
// URL is clean but we have a saved preference - redirect with lang parameter
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('lang', savedLang);
|
|
window.location.replace(url.toString());
|
|
} else if (urlLang) {
|
|
// Save URL language to localStorage
|
|
localStorage.setItem('cv-language', urlLang);
|
|
}
|
|
|
|
// Zoom control initialization now handled by hyperscript in zoom-control.html
|
|
}
|
|
|
|
// =============================================================================
|
|
// SCROLL BEHAVIOR - Now handled by Hyperscript on <body> element
|
|
// =============================================================================
|
|
|
|
// Scroll behavior moved to inline hyperscript on body tag:
|
|
// - Header hide/show based on scroll direction (with 100px threshold)
|
|
// - Back-to-top button visibility (appears after 300px scroll)
|
|
// - At-bottom positioning for fixed buttons (within 50px of page bottom)
|
|
// - Menu visibility coordination when open
|
|
// - State tracking: lastScroll, scrollThreshold, keepHeaderVisible
|
|
//
|
|
// Result: ~59 lines of JavaScript eliminated!
|
|
|
|
// =============================================================================
|
|
// MODALS - Using Native <dialog> Element
|
|
// =============================================================================
|
|
|
|
// Native <dialog> elements handle:
|
|
// - Open/close with .showModal() and .close()
|
|
// - Backdrop clicks (via ::backdrop CSS pseudo-element)
|
|
// - Escape key to close (built-in browser behavior)
|
|
// - Body scroll prevention (automatic with modal dialogs)
|
|
// - Focus trapping (automatic accessibility feature)
|
|
//
|
|
// No JavaScript needed! All modal logic is now in HTML/CSS.
|
|
|
|
// =============================================================================
|
|
// ERROR HANDLING
|
|
// =============================================================================
|
|
|
|
// Error handling utility - CSS handles auto-hide animation
|
|
window.showError = function(message) {
|
|
const errorToast = document.getElementById('error-toast');
|
|
const errorMessage = document.getElementById('error-message');
|
|
|
|
errorMessage.textContent = message;
|
|
errorToast.classList.remove('show'); // Reset if already showing
|
|
|
|
// Trigger reflow to restart animation
|
|
void errorToast.offsetWidth;
|
|
|
|
errorToast.classList.add('show'); // CSS animation handles lifecycle
|
|
};
|
|
|
|
// Close button handler for error toast - removes class to trigger hide
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.closest('.error-close')) {
|
|
document.getElementById('error-toast')?.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// =============================================================================
|
|
// HTMX EVENT HANDLERS
|
|
// =============================================================================
|
|
|
|
function initHTMXHandlers() {
|
|
// HTMX Global Error Handlers
|
|
document.body.addEventListener('htmx:responseError', function(evt) {
|
|
console.error('HTMX Response Error:', evt.detail);
|
|
const lang = document.documentElement.lang;
|
|
const message = lang === 'es'
|
|
? 'Error al cargar el contenido. Por favor, inténtelo de nuevo.'
|
|
: 'Failed to load content. Please try again.';
|
|
window.showError(message);
|
|
});
|
|
|
|
document.body.addEventListener('htmx:sendError', function(evt) {
|
|
console.error('HTMX Send Error:', evt.detail);
|
|
const lang = document.documentElement.lang;
|
|
const message = lang === 'es'
|
|
? 'Error de conexión. Verifique su conexión a internet.'
|
|
: 'Connection error. Please check your internet connection.';
|
|
window.showError(message);
|
|
});
|
|
|
|
document.body.addEventListener('htmx:timeout', function(evt) {
|
|
console.error('HTMX Timeout:', evt.detail);
|
|
const lang = document.documentElement.lang;
|
|
const message = lang === 'es'
|
|
? 'La solicitud tardó demasiado. Por favor, inténtelo de nuevo.'
|
|
: 'Request timed out. Please try again.';
|
|
window.showError(message);
|
|
});
|
|
|
|
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
|
// Smooth scroll to top on language change
|
|
if (evt.detail.target.id === 'cv-content') {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
// Track HTMX navigation events with Matomo
|
|
if (typeof _paq !== 'undefined' && evt.detail.target.id === 'cv-content') {
|
|
// Track language change as virtual pageview
|
|
const lang = new URLSearchParams(window.location.search).get('lang') || 'en';
|
|
_paq.push(['setCustomUrl', window.location.href]);
|
|
_paq.push(['setDocumentTitle', document.title]);
|
|
_paq.push(['trackPageView']);
|
|
}
|
|
});
|
|
|
|
// Log successful swaps for debugging
|
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
|
if (evt.detail.successful) {
|
|
console.log('HTMX request successful:', evt.detail.pathInfo.requestPath);
|
|
}
|
|
});
|
|
}
|
|
|
|
// =============================================================================
|
|
// INITIALIZATION
|
|
// =============================================================================
|
|
|
|
// Initialize everything when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initMenuSystem();
|
|
initPreferences();
|
|
// Scroll behavior now handled by hyperscript on <body>
|
|
initHTMXHandlers();
|
|
});
|
|
|
|
})();
|