more htmx
This commit is contained in:
+276
-90
@@ -3,11 +3,21 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// =============================================================================
|
||||
// GLOBAL VARIABLES
|
||||
// =============================================================================
|
||||
|
||||
// Flag to keep header visible after navigation
|
||||
let keepHeaderVisible = false;
|
||||
|
||||
// =============================================================================
|
||||
// NAVIGATION & MENU SYSTEM
|
||||
// =============================================================================
|
||||
|
||||
// Minimal menu control - CSS handles most logic, JS just bridges hamburger to menu
|
||||
/**
|
||||
* Initialize minimal menu control system
|
||||
* CSS handles most logic, JS just bridges hamburger to menu
|
||||
*/
|
||||
function initMenuSystem() {
|
||||
const hamburgerBtn = document.querySelector('.hamburger-btn');
|
||||
const menu = document.getElementById('navigation-menu');
|
||||
@@ -37,69 +47,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Flag to keep header visible after navigation
|
||||
let keepHeaderVisible = false;
|
||||
|
||||
// Expand/collapse all sections utility functions
|
||||
/**
|
||||
* Expand all <details> sections in the CV
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
window.expandAllSections = function(event) {
|
||||
event.preventDefault();
|
||||
document.querySelectorAll('details').forEach(d => d.setAttribute('open', ''));
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapse all <details> sections in the CV
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
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');
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Close menu when navigation links are clicked
|
||||
* CSS handles scrolling with scroll-behavior: smooth
|
||||
*/
|
||||
function initMenuCloseOnClick() {
|
||||
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
|
||||
// PREFERENCES & LANGUAGE
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Initialize user preferences from localStorage
|
||||
* Handles language, theme, length, and logos persistence across sessions
|
||||
*/
|
||||
function initPreferences() {
|
||||
const paper = document.querySelector('.cv-paper');
|
||||
|
||||
// Handle language preference
|
||||
// Language preference
|
||||
const urlLang = new URLSearchParams(window.location.search).get('lang');
|
||||
const savedLang = localStorage.getItem('cv-language');
|
||||
|
||||
@@ -113,40 +101,62 @@
|
||||
localStorage.setItem('cv-language', urlLang);
|
||||
}
|
||||
|
||||
// Zoom control initialization now handled by hyperscript in zoom-control.html
|
||||
// Apply other preferences from localStorage on page load
|
||||
// This ensures client-side preferences override server defaults
|
||||
const savedTheme = localStorage.getItem('cv-theme');
|
||||
const savedLength = localStorage.getItem('cv-length');
|
||||
const savedLogos = localStorage.getItem('cv-logos');
|
||||
|
||||
// Apply theme preference
|
||||
if (savedTheme === 'clean') {
|
||||
document.body.classList.add('theme-clean');
|
||||
const themeToggles = document.querySelectorAll('#themeToggle, #themeToggleMenu');
|
||||
themeToggles.forEach(toggle => toggle.checked = true);
|
||||
} else if (savedTheme === 'default') {
|
||||
document.body.classList.remove('theme-clean');
|
||||
const themeToggles = document.querySelectorAll('#themeToggle, #themeToggleMenu');
|
||||
themeToggles.forEach(toggle => toggle.checked = false);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply logos preference
|
||||
if (cvPaper && savedLogos !== null) {
|
||||
if (savedLogos === 'true') {
|
||||
cvPaper.classList.add('show-logos');
|
||||
const logoToggles = document.querySelectorAll('#logoToggle, #logoToggleMenu');
|
||||
logoToggles.forEach(toggle => toggle.checked = true);
|
||||
} else {
|
||||
cvPaper.classList.remove('show-logos');
|
||||
const logoToggles = document.querySelectorAll('#logoToggle, #logoToggleMenu');
|
||||
logoToggles.forEach(toggle => toggle.checked = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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
|
||||
/**
|
||||
* Display error toast notification
|
||||
* CSS handles auto-hide animation via @keyframes toastLifecycle
|
||||
* @param {string} message - Error message to display
|
||||
*/
|
||||
window.showError = function(message) {
|
||||
const errorToast = document.getElementById('error-toast');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
@@ -160,21 +170,130 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Close button handler for error toast
|
||||
*/
|
||||
function initErrorToastClose() {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.error-close')) {
|
||||
document.getElementById('error-toast')?.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTMX EVENT HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Initialize HTMX global event handlers
|
||||
* Handles errors, analytics, and post-swap behaviors
|
||||
*/
|
||||
function initHTMXHandlers() {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
if (target && target.tagName === 'BODY') {
|
||||
const desktopToggle = document.getElementById('themeToggle');
|
||||
const mobileToggle = document.getElementById('themeToggleMenu');
|
||||
if (desktopToggle && mobileToggle) {
|
||||
const isClean = document.body.classList.contains('theme-clean');
|
||||
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}`);
|
||||
}
|
||||
|
||||
// Sync logo toggles
|
||||
const desktopLogoToggle = document.getElementById('logoToggle');
|
||||
const mobileLogoToggle = document.getElementById('logoToggleMenu');
|
||||
if (desktopLogoToggle && mobileLogoToggle) {
|
||||
const showLogos = target.classList.contains('show-logos');
|
||||
desktopLogoToggle.checked = showLogos;
|
||||
mobileLogoToggle.checked = showLogos;
|
||||
console.log(`Toggle sync - Logos: desktop=${showLogos}, mobile=${showLogos}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error syncing toggles:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// HTMX Global Error Handlers
|
||||
document.body.addEventListener('htmx:responseError', function(evt) {
|
||||
document.addEventListener('htmx:responseError', function(evt) {
|
||||
console.error('HTMX Response Error:', evt.detail);
|
||||
console.error('Error details:', {
|
||||
xhr: evt.detail.xhr,
|
||||
target: evt.detail.target,
|
||||
requestConfig: evt.detail.requestConfig
|
||||
});
|
||||
const lang = document.documentElement.lang;
|
||||
const message = lang === 'es'
|
||||
? 'Error al cargar el contenido. Por favor, inténtelo de nuevo.'
|
||||
@@ -182,7 +301,7 @@
|
||||
window.showError(message);
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:sendError', function(evt) {
|
||||
document.addEventListener('htmx:sendError', function(evt) {
|
||||
console.error('HTMX Send Error:', evt.detail);
|
||||
const lang = document.documentElement.lang;
|
||||
const message = lang === 'es'
|
||||
@@ -191,7 +310,7 @@
|
||||
window.showError(message);
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:timeout', function(evt) {
|
||||
document.addEventListener('htmx:timeout', function(evt) {
|
||||
console.error('HTMX Timeout:', evt.detail);
|
||||
const lang = document.documentElement.lang;
|
||||
const message = lang === 'es'
|
||||
@@ -200,7 +319,7 @@
|
||||
window.showError(message);
|
||||
});
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
document.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' });
|
||||
@@ -217,7 +336,7 @@
|
||||
});
|
||||
|
||||
// Log successful swaps for debugging
|
||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
document.addEventListener('htmx:afterRequest', function(evt) {
|
||||
if (evt.detail.successful) {
|
||||
console.log('HTMX request successful:', evt.detail.pathInfo.requestPath);
|
||||
}
|
||||
@@ -228,12 +347,79 @@
|
||||
// INITIALIZATION
|
||||
// =============================================================================
|
||||
|
||||
// Initialize everything when DOM is ready
|
||||
/**
|
||||
* Initialize all CV interactive features when DOM is ready
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initMenuSystem();
|
||||
initMenuCloseOnClick();
|
||||
initPreferences();
|
||||
// Scroll behavior now handled by hyperscript on <body>
|
||||
initErrorToastClose();
|
||||
initHTMXHandlers();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 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.
|
||||
|
||||
// =============================================================================
|
||||
// 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)
|
||||
// =============================================================================
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user