Files
MyWebsite/js/main.js
T
itziarZG 5c3f7a7aa2 fix(lab): repair tab DOM structure, restore missing content, stop page jumps
- Fix malformed class attribute on tab buttons container (was breaking
  pb-4 mb-8 dark:border-slate-800 classes; tab buttons sat glued to
  content with no spacing)
- Move 'Trabajo Anterior' details back inside #tab-proyectos where it
  belongs (had been orphaned into #tab-contents after earlier balance
  fixes, causing it to render on every tab)
- Close tab-proyectos correctly so Proyectos and Notes panes show their
  own content instead of inheriting display:none from Infraestructura
- initTabs() now scrolls smoothly to the tab buttons container on switch
  to prevent the 'jump' caused by varying content height
2026-07-28 19:32:28 +02:00

208 lines
6.2 KiB
JavaScript

// ============================================
// Theme Toggle - Dark/Light Mode
// ============================================
/**
* Initialize theme on page load
* Checks localStorage for saved preference, falls back to light mode
*/
function initTheme() {
const savedTheme = localStorage.getItem("theme") || "light";
if (savedTheme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
/**
* Toggle between dark and light theme
* Saves preference to localStorage
*/
function toggleTheme() {
const html = document.documentElement;
if (html.classList.contains("dark")) {
html.classList.remove("dark");
localStorage.setItem("theme", "light");
} else {
html.classList.add("dark");
localStorage.setItem("theme", "dark");
}
}
// ============================================
// Smooth Scrolling
// ============================================
/**
* Scroll to projects section
*/
function scrollToProjects() {
const projectsSection = document.getElementById("projects");
if (projectsSection) {
projectsSection.scrollIntoView({ behavior: "smooth" });
}
}
/**
* Scroll to contact section
*/
function scrollToContact() {
const contactSection = document.getElementById("contact");
if (contactSection) {
contactSection.scrollIntoView({ behavior: "smooth" });
}
}
// ============================================
// Modal Management
// ============================================
/** Element that had focus before a modal was opened */
let _modalPreviousFocus = null;
/** Scrollbar width in px, measured on first open */
let _scrollbarWidth = null;
/**
* Measure and cache the browser scrollbar width so we can compensate
* for layout shift when body.overflow is toggled.
*/
function getScrollbarWidth() {
if (_scrollbarWidth !== null) return _scrollbarWidth;
const outer = document.createElement("div");
outer.style.cssText =
"visibility:hidden;position:absolute;overflow:scroll;width:100px;height:100px;";
document.body.appendChild(outer);
const inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
const w = outer.offsetWidth - inner.offsetWidth;
outer.remove();
_scrollbarWidth = w;
return w;
}
/**
* Open modal by project ID.
* Saves the currently focused element and moves focus into the modal.
* Uses a CSS class (body.modal-open) so the scrollbar gutter stays reserved
* via html { scrollbar-gutter: stable }, preventing page jumps.
* @param {string} projectId - The project identifier
*/
function openModal(projectId) {
const modal = document.getElementById(projectId + "-modal");
if (modal) {
_modalPreviousFocus = document.activeElement;
modal.classList.remove("hidden");
document.body.classList.add("modal-open");
// Defensive fallback for browsers without scrollbar-gutter support
document.body.style.paddingRight = getScrollbarWidth() + "px";
// Move focus to the first focusable element inside the modal.
// PreventScroll keeps the page from jumping when the focused
// element would otherwise trigger a scroll-into-view.
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
);
if (focusable.length > 0) {
focusable[0].focus({ preventScroll: true });
} else {
modal.setAttribute("tabindex", "-1");
modal.focus({ preventScroll: true });
}
}
}
/**
* Close modal by project ID.
* Restores focus to the element that was focused before the modal opened.
* @param {string} projectId - The project identifier
*/
function closeModal(projectId) {
const modal = document.getElementById(projectId + "-modal");
if (modal) {
modal.classList.add("hidden");
document.body.classList.remove("modal-open");
document.body.style.paddingRight = "";
// Return focus to the element that triggered the modal.
// preventScroll keeps the page from jumping back to that element.
if (_modalPreviousFocus) {
try {
_modalPreviousFocus.focus({ preventScroll: true });
} catch (e) {
_modalPreviousFocus.focus();
}
_modalPreviousFocus = null;
}
}
}
// ============================================
// Laboratorio Tabs Management
// ============================================
/**
* Initialize tabs behavior for the Laboratorio section
* When switching tabs, the page scrolls smoothly to the tab buttons container
* to prevent the "jump" caused by tab content height changes.
*/
function initTabs() {
const tabButtons = document.querySelectorAll(".tab-btn");
const tabPanes = document.querySelectorAll(".tab-pane");
tabButtons.forEach((btn) => {
btn.addEventListener("click", () => {
const targetTab = btn.getAttribute("data-tab");
tabButtons.forEach((b) => {
b.classList.remove("bg-primary", "text-white", "shadow-md", "shadow-primary/25");
b.classList.add("text-slate-600", "dark:text-slate-400", "hover:bg-slate-100", "dark:hover:bg-white/5");
});
btn.classList.add("bg-primary", "text-white", "shadow-md", "shadow-primary/25");
btn.classList.remove("text-slate-600", "dark:text-slate-400", "hover:bg-slate-100", "dark:hover:bg-white/5");
tabPanes.forEach((pane) => {
if (pane.id === `tab-${targetTab}`) {
pane.classList.remove("hidden");
pane.classList.add("block");
} else {
pane.classList.remove("block");
pane.classList.add("hidden");
}
});
const tabsContainer = btn.parentElement;
if (tabsContainer && tabsContainer.parentElement) {
tabsContainer.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
});
}
// ============================================
// Event Listeners
// ============================================
/**
* Close modal when ESC key is pressed
*/
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
const modals = document.querySelectorAll(
'[id$="-modal"]:not(.hidden)'
);
modals.forEach((modal) => {
const projectId = modal.id.replace("-modal", "");
closeModal(projectId);
});
}
});
/**
* Initialize theme and tabs
*/
initTheme();
initTabs();