2025-11-11 21:43:12 +00:00
|
|
|
// CV Interactive Features - CSP-Compliant External JavaScript
|
|
|
|
|
// Extracted from inline scripts for security hardening
|
|
|
|
|
(function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
// GLOBAL VARIABLES
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
// Flag to keep header visible after navigation
|
|
|
|
|
let keepHeaderVisible = false;
|
|
|
|
|
|
2025-11-18 19:32:28 +00:00
|
|
|
// Flag to track language switch in progress
|
|
|
|
|
let languageSwitching = false;
|
|
|
|
|
|
|
|
|
|
// Expose for testing (read-only access)
|
|
|
|
|
Object.defineProperty(window, 'languageSwitching', {
|
|
|
|
|
get: () => languageSwitching
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-11 21:43:12 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
// NAVIGATION & MENU SYSTEM
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
/**
|
|
|
|
|
* Initialize minimal menu control system
|
|
|
|
|
* CSS handles most logic, JS just bridges hamburger to menu
|
|
|
|
|
*/
|
2025-11-11 21:43:12 +00:00
|
|
|
function initMenuSystem() {
|
|
|
|
|
const hamburgerBtn = document.querySelector('.hamburger-btn');
|
|
|
|
|
const menu = document.getElementById('navigation-menu');
|
|
|
|
|
|
|
|
|
|
if (!hamburgerBtn || !menu) return;
|
|
|
|
|
|
2025-11-12 19:23:46 +00:00
|
|
|
// Show menu on hamburger hover - CSS handles the rest
|
|
|
|
|
hamburgerBtn.addEventListener('mouseenter', () => menu.classList.add('menu-hover'));
|
2025-11-11 21:43:12 +00:00
|
|
|
|
2025-11-12 19:23:46 +00:00
|
|
|
// Hide menu when leaving hamburger if not hovering menu
|
|
|
|
|
hamburgerBtn.addEventListener('mouseleave', () => {
|
2025-11-11 21:43:12 +00:00
|
|
|
setTimeout(() => {
|
2025-11-12 19:23:46 +00:00
|
|
|
if (!menu.matches(':hover')) menu.classList.remove('menu-hover');
|
2025-11-11 21:43:12 +00:00
|
|
|
}, 100);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-12 19:23:46 +00:00
|
|
|
// Hide menu when leaving menu
|
|
|
|
|
menu.addEventListener('mouseleave', () => menu.classList.remove('menu-hover'));
|
2025-11-11 21:43:12 +00:00
|
|
|
|
2025-11-12 19:23:46 +00:00
|
|
|
// Position submenu dynamically (needed because fixed positioning)
|
2025-11-11 21:43:12 +00:00
|
|
|
const submenuTrigger = document.querySelector('.menu-item-submenu');
|
|
|
|
|
const submenuContent = document.querySelector('.submenu-content');
|
|
|
|
|
if (submenuTrigger && submenuContent) {
|
|
|
|
|
submenuTrigger.addEventListener('mouseenter', function() {
|
2025-11-12 19:23:46 +00:00
|
|
|
submenuContent.style.top = `${this.getBoundingClientRect().top}px`;
|
2025-11-11 21:43:12 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 05:44:40 +00:00
|
|
|
/**
|
2025-11-25 06:00:39 +00:00
|
|
|
* Auto-open sidebar accordions in landscape mobile mode AND desktop
|
|
|
|
|
* Ensures sidebar content is always visible except in portrait mobile
|
2025-11-25 05:44:40 +00:00
|
|
|
*/
|
|
|
|
|
function handleLandscapeAccordions() {
|
2025-11-25 06:00:39 +00:00
|
|
|
function openSidebarAccordionsIfNeeded() {
|
2025-11-25 05:44:40 +00:00
|
|
|
const isLandscape = window.matchMedia('(max-width: 915px) and (orientation: landscape)').matches;
|
2025-11-25 06:00:39 +00:00
|
|
|
const isDesktop = window.matchMedia('(min-width: 769px)').matches;
|
|
|
|
|
const isPortraitMobile = window.matchMedia('(max-width: 768px) and (orientation: portrait)').matches;
|
2025-11-25 05:44:40 +00:00
|
|
|
|
2025-11-25 06:00:39 +00:00
|
|
|
// Open accordions in landscape mobile OR desktop view
|
|
|
|
|
// Keep them closed ONLY in portrait mobile (≤768px)
|
|
|
|
|
if (isLandscape || isDesktop) {
|
2025-11-25 05:44:40 +00:00
|
|
|
document.querySelectorAll('.sidebar-accordion').forEach(accordion => {
|
|
|
|
|
accordion.setAttribute('open', '');
|
|
|
|
|
});
|
2025-11-25 06:00:39 +00:00
|
|
|
} else if (isPortraitMobile) {
|
|
|
|
|
// In portrait mobile, leave them closed (user can expand manually)
|
|
|
|
|
// Don't remove 'open' attribute if user has opened them
|
2025-11-25 05:44:40 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run on load
|
2025-11-25 06:00:39 +00:00
|
|
|
openSidebarAccordionsIfNeeded();
|
2025-11-25 05:44:40 +00:00
|
|
|
|
|
|
|
|
// Run on orientation change
|
|
|
|
|
window.addEventListener('orientationchange', () => {
|
2025-11-25 06:00:39 +00:00
|
|
|
setTimeout(openSidebarAccordionsIfNeeded, 100);
|
2025-11-25 05:44:40 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Run on resize (for desktop browser testing)
|
|
|
|
|
window.addEventListener('resize', () => {
|
2025-11-25 06:00:39 +00:00
|
|
|
openSidebarAccordionsIfNeeded();
|
2025-11-25 05:44:40 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 06:06:10 +00:00
|
|
|
// Menu close on nav click is now handled by scrollToSection() in utils._hs
|
2025-11-11 21:43:12 +00:00
|
|
|
|
|
|
|
|
// =============================================================================
|
2025-11-14 21:38:09 +00:00
|
|
|
// PREFERENCES & LANGUAGE
|
2025-11-11 21:43:12 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
/**
|
|
|
|
|
* Initialize user preferences from localStorage
|
2025-11-15 18:42:35 +00:00
|
|
|
* Handles language, theme, length, and icons persistence across sessions
|
2025-11-14 21:38:09 +00:00
|
|
|
*/
|
2025-11-11 21:43:12 +00:00
|
|
|
function initPreferences() {
|
2025-11-14 21:38:09 +00:00
|
|
|
// Language preference
|
2025-11-11 21:43:12 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
// Apply other preferences from localStorage on page load
|
|
|
|
|
// This ensures client-side preferences override server defaults
|
|
|
|
|
const savedTheme = localStorage.getItem('cv-theme');
|
2025-11-20 11:21:43 +00:00
|
|
|
let savedLength = localStorage.getItem('cv-length');
|
|
|
|
|
let savedIcons = localStorage.getItem('cv-icons');
|
|
|
|
|
|
|
|
|
|
// Migrate old localStorage values to new ones (one-time auto-migration)
|
|
|
|
|
if (savedLength === 'extended') {
|
|
|
|
|
savedLength = 'long';
|
|
|
|
|
localStorage.setItem('cv-length', 'long');
|
|
|
|
|
}
|
|
|
|
|
if (savedIcons === 'true') {
|
|
|
|
|
savedIcons = 'show';
|
|
|
|
|
localStorage.setItem('cv-icons', 'show');
|
|
|
|
|
} else if (savedIcons === 'false') {
|
|
|
|
|
savedIcons = 'hide';
|
|
|
|
|
localStorage.setItem('cv-icons', 'hide');
|
|
|
|
|
}
|
2025-11-14 21:38:09 +00:00
|
|
|
|
|
|
|
|
// Apply theme preference
|
2025-11-19 14:49:28 +00:00
|
|
|
const cvContainer = document.querySelector('.cv-container');
|
2025-11-14 21:38:09 +00:00
|
|
|
if (savedTheme === 'clean') {
|
2025-11-19 14:49:28 +00:00
|
|
|
cvContainer?.classList.add('theme-clean');
|
2025-11-14 21:38:09 +00:00
|
|
|
const themeToggles = document.querySelectorAll('#themeToggle, #themeToggleMenu');
|
|
|
|
|
themeToggles.forEach(toggle => toggle.checked = true);
|
|
|
|
|
} else if (savedTheme === 'default') {
|
2025-11-19 14:49:28 +00:00
|
|
|
cvContainer?.classList.remove('theme-clean');
|
2025-11-14 21:38:09 +00:00
|
|
|
const themeToggles = document.querySelectorAll('#themeToggle, #themeToggleMenu');
|
|
|
|
|
themeToggles.forEach(toggle => toggle.checked = false);
|
|
|
|
|
}
|
2025-11-11 21:43:12 +00:00
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
// Apply length preference
|
|
|
|
|
const cvPaper = document.querySelector('.cv-paper');
|
|
|
|
|
if (cvPaper && savedLength) {
|
|
|
|
|
if (savedLength === 'long') {
|
|
|
|
|
cvPaper.classList.remove('cv-short');
|
|
|
|
|
cvPaper.classList.add('cv-long');
|
|
|
|
|
const lengthToggles = document.querySelectorAll('#lengthToggle, #lengthToggleMenu');
|
|
|
|
|
lengthToggles.forEach(toggle => toggle.checked = true);
|
|
|
|
|
} else {
|
|
|
|
|
cvPaper.classList.remove('cv-long');
|
|
|
|
|
cvPaper.classList.add('cv-short');
|
|
|
|
|
const lengthToggles = document.querySelectorAll('#lengthToggle, #lengthToggleMenu');
|
|
|
|
|
lengthToggles.forEach(toggle => toggle.checked = false);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-11 21:43:12 +00:00
|
|
|
|
2025-11-15 18:42:35 +00:00
|
|
|
// Apply icons preference
|
|
|
|
|
if (cvPaper && savedIcons !== null) {
|
2025-11-20 11:21:43 +00:00
|
|
|
if (savedIcons === 'show') {
|
2025-11-15 18:42:35 +00:00
|
|
|
cvPaper.classList.add('show-icons');
|
|
|
|
|
const iconToggles = document.querySelectorAll('#iconToggle, #iconToggleMenu');
|
|
|
|
|
iconToggles.forEach(toggle => toggle.checked = true);
|
2025-11-14 21:38:09 +00:00
|
|
|
} else {
|
2025-11-15 18:42:35 +00:00
|
|
|
cvPaper.classList.remove('show-icons');
|
|
|
|
|
const iconToggles = document.querySelectorAll('#iconToggle, #iconToggleMenu');
|
|
|
|
|
iconToggles.forEach(toggle => toggle.checked = false);
|
2025-11-14 21:38:09 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-11 21:43:12 +00:00
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// ERROR HANDLING
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
/**
|
|
|
|
|
* Display error toast notification
|
|
|
|
|
* CSS handles auto-hide animation via @keyframes toastLifecycle
|
|
|
|
|
* @param {string} message - Error message to display
|
|
|
|
|
*/
|
2025-11-11 21:43:12 +00:00
|
|
|
window.showError = function(message) {
|
|
|
|
|
const errorToast = document.getElementById('error-toast');
|
|
|
|
|
const errorMessage = document.getElementById('error-message');
|
2025-11-12 19:23:46 +00:00
|
|
|
|
2025-11-11 21:43:12 +00:00
|
|
|
errorMessage.textContent = message;
|
2025-11-12 19:23:46 +00:00
|
|
|
errorToast.classList.remove('show'); // Reset if already showing
|
2025-11-11 21:43:12 +00:00
|
|
|
|
2025-11-12 19:23:46 +00:00
|
|
|
// Trigger reflow to restart animation
|
|
|
|
|
void errorToast.offsetWidth;
|
|
|
|
|
|
|
|
|
|
errorToast.classList.add('show'); // CSS animation handles lifecycle
|
2025-11-11 21:43:12 +00:00
|
|
|
};
|
|
|
|
|
|
2025-11-20 13:00:06 +00:00
|
|
|
/**
|
|
|
|
|
* Display PDF download toast notification
|
|
|
|
|
* Shows progress and completion status for PDF downloads
|
|
|
|
|
* @param {Object} options - Toast configuration
|
|
|
|
|
* @param {string} options.icon - Icon emoji (📥, ✅, ⚠️)
|
|
|
|
|
* @param {string} options.title - Toast title
|
|
|
|
|
* @param {string} options.message - Toast message
|
|
|
|
|
* @param {number} options.duration - Auto-hide duration in ms (default: 5000)
|
|
|
|
|
*/
|
|
|
|
|
window.showPDFToast = function(options = {}) {
|
|
|
|
|
const toast = document.getElementById('pdf-toast');
|
|
|
|
|
const icon = document.getElementById('pdf-toast-icon');
|
|
|
|
|
const title = document.getElementById('pdf-toast-title');
|
|
|
|
|
const message = document.getElementById('pdf-toast-message');
|
|
|
|
|
const progressBar = document.getElementById('pdf-toast-progress');
|
|
|
|
|
|
|
|
|
|
// Set content
|
|
|
|
|
if (options.icon) icon.textContent = options.icon;
|
|
|
|
|
if (options.title) title.textContent = options.title;
|
|
|
|
|
if (options.message) message.textContent = options.message;
|
|
|
|
|
|
|
|
|
|
// Reset animation
|
|
|
|
|
toast.classList.remove('show');
|
|
|
|
|
void toast.offsetWidth; // Trigger reflow
|
|
|
|
|
|
|
|
|
|
// Reset progress bar animation
|
|
|
|
|
if (progressBar) {
|
|
|
|
|
progressBar.style.animation = 'none';
|
|
|
|
|
void progressBar.offsetWidth;
|
|
|
|
|
|
|
|
|
|
// Set duration if provided
|
|
|
|
|
const duration = options.duration || 5000;
|
|
|
|
|
progressBar.style.animation = `progressShrink ${duration}ms linear forwards`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show toast
|
|
|
|
|
toast.classList.add('show');
|
|
|
|
|
|
|
|
|
|
// Auto-hide after duration
|
|
|
|
|
if (options.autoHide !== false) {
|
|
|
|
|
const duration = options.duration || 5000;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
toast.classList.remove('show');
|
|
|
|
|
}, duration);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hide PDF toast immediately
|
|
|
|
|
*/
|
|
|
|
|
window.hidePDFToast = function() {
|
|
|
|
|
const toast = document.getElementById('pdf-toast');
|
|
|
|
|
toast?.classList.remove('show');
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-30 06:06:10 +00:00
|
|
|
// Error toast close handled by hyperscript inline in error-toast.html
|
2025-11-12 18:59:48 +00:00
|
|
|
|
2025-11-11 21:43:12 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
// HTMX EVENT HANDLERS
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
/**
|
|
|
|
|
* Initialize HTMX global event handlers
|
|
|
|
|
* Handles errors, analytics, and post-swap behaviors
|
|
|
|
|
*/
|
2025-11-11 21:43:12 +00:00
|
|
|
function initHTMXHandlers() {
|
2025-11-14 21:38:09 +00:00
|
|
|
// Variable to store scroll position for swaps that should preserve position
|
|
|
|
|
let savedScrollPosition = 0;
|
|
|
|
|
let shouldRestoreScroll = false;
|
|
|
|
|
|
|
|
|
|
// Save scroll position before swap
|
|
|
|
|
document.addEventListener('htmx:beforeSwap', function(evt) {
|
|
|
|
|
try {
|
|
|
|
|
const target = evt.detail.target;
|
|
|
|
|
|
|
|
|
|
// Only preserve scroll for toggle operations (not language changes)
|
|
|
|
|
// Language changes target #cv-content, toggles target .cv-paper or body
|
|
|
|
|
if (target && (target.classList?.contains('cv-paper') || target.tagName === 'BODY')) {
|
|
|
|
|
savedScrollPosition = window.pageYOffset;
|
|
|
|
|
shouldRestoreScroll = true;
|
|
|
|
|
} else {
|
|
|
|
|
shouldRestoreScroll = false;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error in htmx:beforeSwap handler:', e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Restore scroll position after swap (only for toggles)
|
|
|
|
|
document.addEventListener('htmx:afterSettle', function(evt) {
|
|
|
|
|
try {
|
|
|
|
|
if (shouldRestoreScroll && savedScrollPosition >= 0) {
|
|
|
|
|
window.scrollTo(0, savedScrollPosition);
|
|
|
|
|
savedScrollPosition = 0;
|
|
|
|
|
shouldRestoreScroll = false;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error in htmx:afterSettle handler:', e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-18 19:32:28 +00:00
|
|
|
// Skeleton loader for language transitions
|
|
|
|
|
// Add .loading class when language button is clicked
|
|
|
|
|
document.addEventListener('htmx:beforeRequest', function(evt) {
|
|
|
|
|
try {
|
|
|
|
|
const element = evt.detail.elt;
|
|
|
|
|
if (element && element.classList && element.classList.contains('selector-btn')) {
|
|
|
|
|
// Set flag to track language switching
|
|
|
|
|
languageSwitching = true;
|
|
|
|
|
|
|
|
|
|
// Add loading class to page containers
|
|
|
|
|
const page1 = document.getElementById('cv-inner-content-page-1');
|
|
|
|
|
const page2 = document.getElementById('cv-inner-content-page-2');
|
|
|
|
|
if (page1) page1.classList.add('loading');
|
|
|
|
|
if (page2) page2.classList.add('loading');
|
|
|
|
|
|
|
|
|
|
console.log('Skeleton loader: Added .loading class to page containers');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error in skeleton loader beforeRequest handler:', e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Remove .loading class after language transition completes
|
|
|
|
|
document.addEventListener('htmx:afterSettle', function(evt) {
|
|
|
|
|
try {
|
|
|
|
|
if (languageSwitching) {
|
|
|
|
|
// Wait for final render to complete
|
|
|
|
|
setTimeout(function() {
|
|
|
|
|
const page1 = document.getElementById('cv-inner-content-page-1');
|
|
|
|
|
const page2 = document.getElementById('cv-inner-content-page-2');
|
|
|
|
|
if (page1) page1.classList.remove('loading');
|
|
|
|
|
if (page2) page2.classList.remove('loading');
|
|
|
|
|
|
|
|
|
|
// Reset flag
|
|
|
|
|
languageSwitching = false;
|
|
|
|
|
|
|
|
|
|
console.log('Skeleton loader: Removed .loading class from page containers');
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error in skeleton loader afterSettle handler:', e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
// Sync toggle states between desktop and mobile menu
|
|
|
|
|
document.addEventListener('htmx:afterSwap', function(evt) {
|
|
|
|
|
try {
|
|
|
|
|
// After any swap, sync the corresponding elements
|
|
|
|
|
const target = evt.detail.target;
|
|
|
|
|
|
|
|
|
|
// Sync language buttons when CV page content swaps
|
|
|
|
|
if (target && target.classList && target.classList.contains('cv-page-content-wrapper')) {
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const currentLang = urlParams.get('lang') || 'en';
|
|
|
|
|
const enBtn = document.querySelector('button[aria-label="English"]');
|
|
|
|
|
const esBtn = document.querySelector('button[aria-label="Español"]');
|
|
|
|
|
if (enBtn && esBtn) {
|
|
|
|
|
if (currentLang === 'en') {
|
|
|
|
|
enBtn.classList.add('active');
|
|
|
|
|
esBtn.classList.remove('active');
|
|
|
|
|
} else {
|
|
|
|
|
esBtn.classList.add('active');
|
|
|
|
|
enBtn.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sync theme toggles (body swap) - though theme now uses hyperscript
|
2025-11-19 14:49:28 +00:00
|
|
|
if (target && target.classList.contains('cv-container')) {
|
2025-11-14 21:38:09 +00:00
|
|
|
const desktopToggle = document.getElementById('themeToggle');
|
|
|
|
|
const mobileToggle = document.getElementById('themeToggleMenu');
|
|
|
|
|
if (desktopToggle && mobileToggle) {
|
2025-11-19 14:49:28 +00:00
|
|
|
const isClean = document.querySelector('.cv-container')?.classList.contains('theme-clean');
|
2025-11-14 21:38:09 +00:00
|
|
|
desktopToggle.checked = isClean;
|
|
|
|
|
mobileToggle.checked = isClean;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sync length and logo toggles (.cv-paper swap)
|
|
|
|
|
if (target && target.classList && target.classList.contains('cv-paper')) {
|
|
|
|
|
// Sync length toggles
|
|
|
|
|
const desktopToggle = document.getElementById('lengthToggle');
|
|
|
|
|
const mobileToggle = document.getElementById('lengthToggleMenu');
|
|
|
|
|
if (desktopToggle && mobileToggle) {
|
|
|
|
|
const isLong = target.classList.contains('cv-long');
|
|
|
|
|
desktopToggle.checked = isLong;
|
|
|
|
|
mobileToggle.checked = isLong;
|
|
|
|
|
console.log(`Toggle sync - Length: desktop=${isLong}, mobile=${isLong}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 18:42:35 +00:00
|
|
|
// Sync icon toggles
|
|
|
|
|
const desktopIconToggle = document.getElementById('iconToggle');
|
|
|
|
|
const mobileIconToggle = document.getElementById('iconToggleMenu');
|
|
|
|
|
if (desktopIconToggle && mobileIconToggle) {
|
|
|
|
|
const showIcons = target.classList.contains('show-icons');
|
|
|
|
|
desktopIconToggle.checked = showIcons;
|
|
|
|
|
mobileIconToggle.checked = showIcons;
|
|
|
|
|
console.log(`Toggle sync - Icons: desktop=${showIcons}, mobile=${showIcons}`);
|
2025-11-14 21:38:09 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error syncing toggles:', e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-11 21:43:12 +00:00
|
|
|
// HTMX Global Error Handlers
|
2025-11-14 21:38:09 +00:00
|
|
|
document.addEventListener('htmx:responseError', function(evt) {
|
2025-11-11 21:43:12 +00:00
|
|
|
console.error('HTMX Response Error:', evt.detail);
|
2025-11-14 21:38:09 +00:00
|
|
|
console.error('Error details:', {
|
|
|
|
|
xhr: evt.detail.xhr,
|
|
|
|
|
target: evt.detail.target,
|
|
|
|
|
requestConfig: evt.detail.requestConfig
|
|
|
|
|
});
|
2025-11-11 21:43:12 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
document.addEventListener('htmx:sendError', function(evt) {
|
2025-11-11 21:43:12 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
document.addEventListener('htmx:timeout', function(evt) {
|
2025-11-11 21:43:12 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
document.addEventListener('htmx:afterSwap', function(evt) {
|
2025-11-11 21:43:12 +00:00
|
|
|
// 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
|
2025-11-14 21:38:09 +00:00
|
|
|
document.addEventListener('htmx:afterRequest', function(evt) {
|
2025-11-11 21:43:12 +00:00
|
|
|
if (evt.detail.successful) {
|
|
|
|
|
console.log('HTMX request successful:', evt.detail.pathInfo.requestPath);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// INITIALIZATION
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
/**
|
|
|
|
|
* Initialize all CV interactive features when DOM is ready
|
|
|
|
|
*/
|
2025-11-11 21:43:12 +00:00
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
initMenuSystem();
|
|
|
|
|
initPreferences();
|
|
|
|
|
initHTMXHandlers();
|
2025-11-25 05:44:40 +00:00
|
|
|
handleLandscapeAccordions(); // Auto-open sidebar accordions in landscape mode
|
2025-11-30 04:35:16 +00:00
|
|
|
// Note: Scroll behavior now handled by hyperscript in index.html body tag
|
2025-11-30 06:03:45 +00:00
|
|
|
// Note: Zoom control buttons now handled by hyperscript (zoom._hs)
|
2025-11-30 04:35:16 +00:00
|
|
|
// initScrollBehaviorJS() removed - hyperscript handleScroll() is preferred
|
2025-11-11 21:43:12 +00:00
|
|
|
});
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
// HYPERSCRIPT-POWERED FEATURES (NO JS NEEDED)
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
// The following features have been moved to hyperscript for better
|
|
|
|
|
// maintainability, declarative syntax, and cleaner HTML templates.
|
|
|
|
|
// All hyperscript functions are defined in /static/hyperscript/functions._hs
|
|
|
|
|
|
|
|
|
|
// ZOOM CONTROL (Phase 5 - Eliminated ~343 lines)
|
|
|
|
|
// -----------------------------------------------
|
|
|
|
|
// Now handled by hyperscript in zoom-control.html
|
|
|
|
|
// Features:
|
|
|
|
|
// - 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)
|
|
|
|
|
|
|
|
|
|
// SCROLL BEHAVIOR (Phase 6 - Eliminated ~59 lines)
|
|
|
|
|
// ------------------------------------------------
|
|
|
|
|
// Now handled by hyperscript on <body> element in index.html
|
|
|
|
|
// Functions: initScrollBehavior(), handleScroll()
|
|
|
|
|
// Features:
|
|
|
|
|
// - Header hide/show based on scroll direction (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
|
|
|
|
|
|
|
|
|
|
// PRINT FUNCTION (Phase 6 - Eliminated ~44 lines, Fixed bug)
|
|
|
|
|
// ----------------------------------------------------------
|
|
|
|
|
// Now handled by hyperscript in action-buttons.html and hamburger-menu.html
|
|
|
|
|
// Function: printFriendly()
|
|
|
|
|
// Features:
|
|
|
|
|
// - 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)
|
|
|
|
|
|
|
|
|
|
// MODALS (Phase 4A - Eliminated ~47 lines)
|
|
|
|
|
// ----------------------------------------
|
|
|
|
|
// Now handled by native HTML5 <dialog> element
|
|
|
|
|
// Features:
|
|
|
|
|
// - 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.
|
|
|
|
|
|
2025-11-20 12:14:53 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
// PDF MODAL HELPER FUNCTION
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Opens the PDF download modal
|
|
|
|
|
* Called from references section links with action="downloadPDF"
|
|
|
|
|
*/
|
|
|
|
|
window.openPdfModal = function() {
|
|
|
|
|
const pdfModal = document.querySelector('#pdf-modal');
|
|
|
|
|
if (pdfModal) {
|
2025-11-22 16:23:05 +00:00
|
|
|
// Apply mobile centering via inline styles with !important (overrides CSS !important)
|
|
|
|
|
if (window.innerWidth <= 768) {
|
|
|
|
|
// Reset inset FIRST (before setting top/left)
|
|
|
|
|
pdfModal.style.setProperty('inset', 'auto', 'important');
|
|
|
|
|
pdfModal.style.setProperty('margin', '0', 'important');
|
|
|
|
|
// Now set positioning with !important (after inset reset)
|
|
|
|
|
pdfModal.style.setProperty('position', 'fixed', 'important');
|
|
|
|
|
pdfModal.style.setProperty('top', '50%', 'important');
|
|
|
|
|
pdfModal.style.setProperty('left', '50%', 'important');
|
|
|
|
|
pdfModal.style.setProperty('right', 'auto', 'important');
|
|
|
|
|
pdfModal.style.setProperty('bottom', 'auto', 'important');
|
|
|
|
|
pdfModal.style.setProperty('transform', 'translate(-50%, -50%)', 'important');
|
|
|
|
|
}
|
2025-11-20 12:14:53 +00:00
|
|
|
pdfModal.showModal();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-14 21:38:09 +00:00
|
|
|
// =============================================================================
|
|
|
|
|
// TOTAL REDUCTION SUMMARY
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// Baseline: 954 lines
|
|
|
|
|
// Phase 4A: -285 lines (Native APIs, CSS, HTMX)
|
|
|
|
|
// Phase 5: -343 lines (Hyperscript zoom control)
|
|
|
|
|
// Phase 6: -87 lines (Hyperscript scroll & print + organization)
|
|
|
|
|
// Current: 239 lines (74.9% reduction)
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
2025-11-11 21:43:12 +00:00
|
|
|
})();
|