// CV Interactive Features - CSP-Compliant External JavaScript // Extracted from inline scripts for security hardening (function() { 'use strict'; // ============================================================================= // NAVIGATION & MENU SYSTEM // ============================================================================= // Hover-based menu control function initMenuSystem() { const hamburgerBtn = document.querySelector('.hamburger-btn'); const menu = document.getElementById('navigation-menu'); if (!hamburgerBtn || !menu) return; // Show menu on hamburger hover hamburgerBtn.addEventListener('mouseenter', function() { menu.classList.add('menu-hover'); hamburgerBtn.setAttribute('aria-expanded', 'true'); }); // Hide menu when leaving hamburger (only if not hovering menu) hamburgerBtn.addEventListener('mouseleave', function() { setTimeout(() => { if (!menu.matches(':hover')) { menu.classList.remove('menu-hover'); hamburgerBtn.setAttribute('aria-expanded', 'false'); } }, 100); }); // Hide menu when leaving menu itself menu.addEventListener('mouseleave', function() { menu.classList.remove('menu-hover'); hamburgerBtn.setAttribute('aria-expanded', 'false'); }); // Position submenu dynamically const submenuTrigger = document.querySelector('.menu-item-submenu'); const submenuContent = document.querySelector('.submenu-content'); if (submenuTrigger && submenuContent) { submenuTrigger.addEventListener('mouseenter', function() { const triggerRect = submenuTrigger.getBoundingClientRect(); submenuContent.style.top = `${triggerRect.top}px`; }); } } // Legacy toggle function - kept for compatibility window.toggleMenu = function() { const menu = document.getElementById('navigation-menu'); const btn = document.querySelector('.hamburger-btn'); if (menu.classList.contains('menu-open')) { menu.classList.remove('menu-open'); btn.setAttribute('aria-expanded', 'false'); } else { menu.classList.add('menu-open'); btn.setAttribute('aria-expanded', 'true'); } }; // Flag to keep header visible after navigation let keepHeaderVisible = false; // Toggle sidebar accordion (mobile only) window.toggleSidebar = function(header) { const content = header.nextElementSibling; const isActive = header.classList.contains('active'); if (isActive) { // Close header.classList.remove('active'); content.classList.remove('active'); } else { // Open header.classList.add('active'); content.classList.add('active'); } }; // Expand all sections window.expandAllSections = function(event) { event.preventDefault(); const allDetails = document.querySelectorAll('details'); allDetails.forEach(detail => { detail.setAttribute('open', ''); }); }; // Collapse all sections window.collapseAllSections = function(event) { event.preventDefault(); const allDetails = document.querySelectorAll('details'); allDetails.forEach(detail => { detail.removeAttribute('open'); }); }; // Toggle submenu - no longer needed for hover, but kept for compatibility window.toggleSubmenu = function(event) { event.preventDefault(); const submenuContainer = event.currentTarget.parentElement; submenuContainer.classList.toggle('submenu-open'); }; // Scroll to section smoothly window.scrollToSection = function(sectionId) { event.preventDefault(); // Prevent default anchor behavior const section = document.getElementById(sectionId); if (section) { // Ensure header is visible before scrolling const actionBar = document.querySelector('.action-bar'); const navMenu = document.querySelector('.navigation-menu'); actionBar.classList.remove('header-hidden'); navMenu.classList.remove('header-hidden'); // Set flag to keep header visible keepHeaderVisible = true; // Close menu after clicking navMenu.classList.remove('menu-open'); document.querySelector('.hamburger-btn').setAttribute('aria-expanded', 'false'); // Wait a bit for header to be visible, then calculate offset setTimeout(() => { const actionBarHeight = actionBar.offsetHeight; const offset = actionBarHeight + 20; // Add 20px padding const elementPosition = section.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - offset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); }, 100); } }; // Close menu when clicking outside (only for legacy click-opened menus) function initClickOutsideHandler() { document.addEventListener('click', function(event) { const menu = document.getElementById('navigation-menu'); const btn = document.querySelector('.hamburger-btn'); if (menu && btn && menu.classList.contains('menu-open')) { if (!menu.contains(event.target) && !btn.contains(event.target)) { menu.classList.remove('menu-open'); btn.setAttribute('aria-expanded', 'false'); } } }); } // ============================================================================= // LANGUAGE & PREFERENCES // ============================================================================= // Track if URL originally had lang parameter const urlHadLangParam = new URLSearchParams(window.location.search).has('lang'); window.selectLanguage = function(lang) { // Save language preference to localStorage localStorage.setItem('cv-language', lang); // Reload page with new language parameter const url = new URL(window.location); url.searchParams.set('lang', lang); window.location.href = url.toString(); }; window.toggleCVLength = function() { const headerToggle = document.getElementById('lengthToggle'); const menuToggle = document.getElementById('lengthToggleMenu'); const paper = document.querySelector('.cv-paper'); // Get the state from whichever toggle was clicked const isChecked = event?.target?.id === 'lengthToggleMenu' ? menuToggle?.checked : headerToggle?.checked; // Sync both toggles if (headerToggle) headerToggle.checked = isChecked; if (menuToggle) menuToggle.checked = isChecked; // Save current scroll position const currentScrollY = window.scrollY || window.pageYOffset; if (isChecked) { paper.classList.add('cv-long'); paper.classList.remove('cv-short'); localStorage.setItem('cv-length', 'long'); } else { paper.classList.add('cv-short'); paper.classList.remove('cv-long'); localStorage.setItem('cv-length', 'short'); } // Restore scroll position after DOM updates requestAnimationFrame(() => { window.scrollTo(0, currentScrollY); }); }; window.toggleLogos = function() { const headerToggle = document.getElementById('logoToggle'); const menuToggle = document.getElementById('logoToggleMenu'); const paper = document.querySelector('.cv-paper'); // Get the state from whichever toggle was clicked const isChecked = event?.target?.id === 'logoToggleMenu' ? menuToggle?.checked : headerToggle?.checked; // Sync both toggles if (headerToggle) headerToggle.checked = isChecked; if (menuToggle) menuToggle.checked = isChecked; // Save current scroll position const currentScrollY = window.scrollY || window.pageYOffset; if (isChecked) { paper.classList.add('show-logos'); localStorage.setItem('cv-logos', 'show'); } else { paper.classList.remove('show-logos'); localStorage.setItem('cv-logos', 'hide'); } // Restore scroll position after DOM updates requestAnimationFrame(() => { window.scrollTo(0, currentScrollY); }); }; window.toggleTheme = function() { const headerToggle = document.getElementById('themeToggle'); const menuToggle = document.getElementById('themeToggleMenu'); const container = document.querySelector('.cv-container'); // Get the state from whichever toggle was clicked const isChecked = event?.target?.id === 'themeToggleMenu' ? menuToggle?.checked : headerToggle?.checked; // Sync both toggles if (headerToggle) headerToggle.checked = isChecked; if (menuToggle) menuToggle.checked = isChecked; if (isChecked) { container.classList.add('theme-clean'); localStorage.setItem('cv-theme', 'clean'); } else { container.classList.remove('theme-clean'); localStorage.setItem('cv-theme', 'default'); } }; // ============================================================================= // ZOOM CONTROL // ============================================================================= /** * Check if we're on mobile viewport * @returns {boolean} True if mobile (viewport <= 768px) */ function isMobileView() { return window.innerWidth <= 768; } /** * Initialize zoom control on page load * Restores saved zoom level from localStorage (desktop only) */ function initZoomControl() { const slider = document.getElementById('zoom-slider'); const resetBtn = document.getElementById('zoom-reset'); const zoomWrapper = document.getElementById('zoom-wrapper'); if (!slider || !zoomWrapper) return; // On mobile, always use 100% zoom (zoom control is hidden anyway) if (isMobileView()) { slider.value = 100; applyZoom(100, false); return; // Skip event listeners on mobile } // Desktop: Restore saved zoom level from localStorage const savedZoom = localStorage.getItem('cv-zoom'); if (savedZoom) { const zoomValue = parseInt(savedZoom, 10); slider.value = zoomValue; applyZoom(zoomValue, false); // false = don't save (already loaded from storage) } // Real-time slider updates - immediate, smooth analog experience slider.addEventListener('input', function(e) { const zoomValue = parseInt(e.target.value, 10); // Apply zoom and update display immediately for smooth analog feel updateZoomDisplay(zoomValue); applyZoom(zoomValue, true); }); // Reset button if (resetBtn) { resetBtn.addEventListener('click', function() { slider.value = 100; applyZoom(100, true); slider.focus(); // Return focus to slider for accessibility }); } // Keyboard shortcuts (Ctrl/Cmd + Plus/Minus/0) document.addEventListener('keydown', function(e) { if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { if (e.key === '=' || e.key === '+') { e.preventDefault(); incrementZoom(10); } else if (e.key === '-') { e.preventDefault(); incrementZoom(-10); } else if (e.key === '0') { e.preventDefault(); slider.value = 100; applyZoom(100, true); } } }); // Handle window resize - reset zoom when switching to mobile let resizeTimeout; window.addEventListener('resize', function() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(function() { if (isMobileView()) { // Reset to 100% zoom when switching to mobile slider.value = 100; applyZoom(100, false); } }, 250); // Debounce resize events }); } /** * Apply zoom transformation to CV paper * @param {number} zoomValue - Zoom percentage (25-175, centered at 100) * @param {boolean} saveToStorage - Whether to persist to localStorage */ function applyZoom(zoomValue, saveToStorage = true) { const zoomWrapper = document.getElementById('zoom-wrapper'); if (!zoomWrapper) return; // Convert percentage to decimal (100 = 1.0, 50 = 0.5, etc.) const zoomLevel = zoomValue / 100; requestAnimationFrame(() => { // Use CSS zoom property - it properly affects layout and extends beyond viewport zoomWrapper.style.zoom = zoomLevel; // When zoom > 100%, allow the wrapper to expand beyond viewport width // Set width to accommodate the expanded content without bounds if (zoomLevel > 1) { // Set width to auto to allow natural expansion zoomWrapper.style.width = 'auto'; zoomWrapper.style.minWidth = '100%'; // Remove max-width constraint to allow horizontal expansion zoomWrapper.style.maxWidth = 'none'; } else { // Reset to default when zoom <= 100% zoomWrapper.style.width = ''; zoomWrapper.style.minWidth = ''; zoomWrapper.style.maxWidth = ''; } // Reset zoom on fixed buttons so they stay same size const backToTopBtn = document.getElementById('back-to-top'); const infoBtn = document.getElementById('info-button'); const inverseZoom = 1 / zoomLevel; if (backToTopBtn) backToTopBtn.style.zoom = inverseZoom; if (infoBtn) infoBtn.style.zoom = inverseZoom; // Update display updateZoomDisplay(zoomValue); // Save to localStorage if (saveToStorage) { localStorage.setItem('cv-zoom', zoomValue.toString()); } // Update zoom control position for horizontal scroll updateZoomControlPosition(); }); } /** * Update visual display and ARIA attributes * @param {number} zoomValue - Current zoom percentage */ function updateZoomDisplay(zoomValue) { const slider = document.getElementById('zoom-slider'); const display = document.getElementById('zoom-value-current'); const resetBtn = document.getElementById('zoom-reset'); if (display) { display.textContent = zoomValue; } if (slider) { slider.setAttribute('aria-valuenow', zoomValue); slider.setAttribute('aria-valuetext', `${zoomValue}%`); } // Add/remove class to enable green hover only when zoom is not 100 if (resetBtn) { if (zoomValue !== 100) { resetBtn.classList.add('zoom-not-default'); } else { resetBtn.classList.remove('zoom-not-default'); } } } /** * Increment/decrement zoom by step amount * @param {number} step - Amount to change (positive or negative) */ function incrementZoom(step) { const slider = document.getElementById('zoom-slider'); if (!slider) return; const currentZoom = parseInt(slider.value, 10); const newZoom = Math.min(175, Math.max(25, currentZoom + step)); slider.value = newZoom; applyZoom(newZoom, true); } /** * Update zoom control position based on horizontal scroll * This keeps the zoom control centered relative to the visible viewport */ function updateZoomControlPosition() { const zoomControl = document.getElementById('zoom-control'); if (!zoomControl || isMobileView()) return; // Only adjust if zoom control is in default centered position // (not dragged to a custom position) const savedPosition = localStorage.getItem('cv-zoom-position'); if (savedPosition) return; // Don't adjust if user has dragged it // Get current horizontal scroll position const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; // Update left position to account for horizontal scroll if (scrollLeft > 0) { // Adjust position to stay centered in viewport during horizontal scroll zoomControl.style.left = `calc(50% + ${scrollLeft}px)`; } else { // Reset to center when scroll is at start zoomControl.style.left = '50%'; } } /** * 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-menu-btn'); if (zoomControl) { zoomControl.style.display = 'none'; localStorage.setItem('cv-zoom-visible', 'false'); } if (showButton) { showButton.style.display = 'block'; } } /** * Show zoom control and hide menu button (global function for onclick) */ window.showZoomControl = function(event) { if (event) event.preventDefault(); // Prevent default link behavior const zoomControl = document.getElementById('zoom-control'); const showButton = document.getElementById('show-zoom-menu-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-menu-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 = 'block'; } 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 Friendly - Apply Clean Theme + Short Version for minimal printing window.printFriendly = function() { const container = document.querySelector('.cv-container'); const paper = document.querySelector('.cv-paper'); const wasClean = container.classList.contains('theme-clean'); const wasLong = paper.classList.contains('cv-long'); // Store current zoom const currentZoom = localStorage.getItem('cv-zoom') || '100'; // Apply clean theme for minimal print (no sidebars, no header, no icons) if (!wasClean) { container.classList.add('theme-clean'); } // Force SHORT version for print (hide detailed content) paper.classList.remove('cv-long'); paper.classList.add('cv-short'); // Temporarily reset zoom for printing if (paper) { paper.style.transform = 'scale(1)'; } // Small delay to let CSS apply setTimeout(() => { window.print(); // Restore original theme and length after print dialog closes setTimeout(() => { if (!wasClean) { container.classList.remove('theme-clean'); } // Restore original length if (wasLong) { paper.classList.remove('cv-short'); paper.classList.add('cv-long'); } // Restore zoom if (paper && currentZoom !== '100') { applyZoom(parseInt(currentZoom, 10), false); } }, 100); }, 50); }; // ============================================================================= // 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); } // Restore CV length preference const savedLength = localStorage.getItem('cv-length') || 'short'; const lengthChecked = savedLength === 'long'; if (lengthChecked) { paper.classList.add('cv-long'); paper.classList.remove('cv-short'); } else { paper.classList.add('cv-short'); paper.classList.remove('cv-long'); } // Sync both header and menu toggles const headerLengthToggle = document.getElementById('lengthToggle'); const menuLengthToggle = document.getElementById('lengthToggleMenu'); if (headerLengthToggle) headerLengthToggle.checked = lengthChecked; if (menuLengthToggle) menuLengthToggle.checked = lengthChecked; // Restore logos preference const savedLogos = localStorage.getItem('cv-logos') || 'show'; const logosChecked = savedLogos === 'show'; if (logosChecked) { paper.classList.add('show-logos'); } else { paper.classList.remove('show-logos'); } // Sync both header and menu toggles const headerLogoToggle = document.getElementById('logoToggle'); const menuLogoToggle = document.getElementById('logoToggleMenu'); if (headerLogoToggle) headerLogoToggle.checked = logosChecked; if (menuLogoToggle) menuLogoToggle.checked = logosChecked; // Restore theme preference const savedTheme = localStorage.getItem('cv-theme') || 'default'; const themeChecked = savedTheme === 'clean'; // Sync both header and menu toggles const headerThemeToggle = document.getElementById('themeToggle'); const menuThemeToggle = document.getElementById('themeToggleMenu'); if (headerThemeToggle) headerThemeToggle.checked = themeChecked; if (menuThemeToggle) menuThemeToggle.checked = themeChecked; if (themeChecked) { window.toggleTheme(); } // Initialize zoom control (zoom level, event listeners) initZoomControl(); // Initialize zoom visibility state (show/hide based on localStorage) initZoomVisibility(); // Initialize zoom dragging (make draggable, restore position) initZoomDragging(); } // ============================================================================= // SCROLL BEHAVIOR // ============================================================================= function initScrollBehavior() { let lastScrollTop = 0; let scrollThreshold = 100; // Start hiding after 100px scroll window.addEventListener('scroll', function() { const actionBar = document.querySelector('.action-bar'); const navMenu = document.querySelector('.navigation-menu'); const backToTopBtn = document.getElementById('back-to-top'); const infoBtn = document.querySelector('.info-button'); const currentScroll = window.pageYOffset || document.documentElement.scrollTop; const isMenuOpen = navMenu.classList.contains('menu-open'); // Update zoom control position on horizontal scroll updateZoomControlPosition(); // Check if at bottom of page (within 50px threshold) const scrollHeight = document.documentElement.scrollHeight; const clientHeight = document.documentElement.clientHeight; const isAtBottom = (scrollHeight - currentScroll - clientHeight) < 50; // If scrolling up, reset the keepHeaderVisible flag if (currentScroll < lastScrollTop) { keepHeaderVisible = false; } // Hide/show header based on scroll direction if (currentScroll > scrollThreshold) { if (currentScroll > lastScrollTop && !keepHeaderVisible) { // Scrolling down - hide header (only if keepHeaderVisible is false) actionBar.classList.add('header-hidden'); // Only hide menu if it's open if (isMenuOpen) { navMenu.classList.add('header-hidden'); } } else { // Scrolling up - show header actionBar.classList.remove('header-hidden'); // Only show menu if it's open if (isMenuOpen) { navMenu.classList.remove('header-hidden'); } } } else { // At top - always show header actionBar.classList.remove('header-hidden'); // Only affect menu if it's open if (isMenuOpen) { navMenu.classList.remove('header-hidden'); } } // Show/hide back to top button if (currentScroll > 300) { backToTopBtn.style.display = 'flex'; } else { backToTopBtn.style.display = 'none'; } // Add/remove at-bottom class for both buttons if (isAtBottom) { if (backToTopBtn) backToTopBtn.classList.add('at-bottom'); if (infoBtn) infoBtn.classList.add('at-bottom'); } else { if (backToTopBtn) backToTopBtn.classList.remove('at-bottom'); if (infoBtn) infoBtn.classList.remove('at-bottom'); } lastScrollTop = currentScroll <= 0 ? 0 : currentScroll; }, false); // Back to top button click handler const backToTopBtn = document.getElementById('back-to-top'); if (backToTopBtn) { backToTopBtn.addEventListener('click', function() { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } } // ============================================================================= // MODALS // ============================================================================= // Info Modal Functions window.openInfoModal = function() { const modal = document.getElementById('info-modal'); modal.classList.add('active'); document.body.style.overflow = 'hidden'; // Prevent scrolling when modal is open }; window.closeInfoModal = function() { const modal = document.getElementById('info-modal'); modal.classList.remove('active'); document.body.style.overflow = ''; // Restore scrolling }; window.closeInfoModalOnBackdrop = function(event) { if (event.target.id === 'info-modal') { window.closeInfoModal(); } }; // PDF Modal Functions window.openPdfModal = function() { const modal = document.getElementById('pdf-modal'); modal.classList.add('active'); document.body.style.overflow = 'hidden'; // Prevent scrolling when modal is open }; window.closePdfModal = function() { const modal = document.getElementById('pdf-modal'); modal.classList.remove('active'); document.body.style.overflow = ''; // Restore scrolling }; window.closePdfModalOnBackdrop = function(event) { if (event.target.id === 'pdf-modal') { window.closePdfModal(); } }; // Close modals with Escape key function initModalKeyHandlers() { document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { window.closeInfoModal(); window.closePdfModal(); } }); } // ============================================================================= // ERROR HANDLING // ============================================================================= // Error handling utility window.showError = function(message) { const errorToast = document.getElementById('error-toast'); const errorMessage = document.getElementById('error-message'); errorMessage.textContent = message; errorToast.style.display = 'flex'; // Auto-hide after 5 seconds setTimeout(() => { errorToast.style.display = 'none'; }, 5000); }; // ============================================================================= // 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(); initClickOutsideHandler(); initPreferences(); initScrollBehavior(); initModalKeyHandlers(); initHTMXHandlers(); }); })();