Spaces:
Running
Running
// Animated Tabs JavaScript - Blinkist Style | |
class AnimatedTabs { | |
constructor(container) { | |
this.container = container; | |
this.tabButtons = container.querySelectorAll('.tab-button'); | |
this.tabPanels = container.querySelectorAll('.tab-panel'); | |
this.tabSlider = container.querySelector('.tab-slider'); | |
this.isAnimating = false; | |
this.init(); | |
} | |
init() { | |
this.setInitialState(); | |
this.bindEvents(); | |
this.addResponsiveBehavior(); | |
} | |
setInitialState() { | |
// Set initial slider position | |
const activeButton = this.container.querySelector('.tab-button.active'); | |
if (activeButton && this.tabSlider) { | |
this.updateSliderPosition(activeButton); | |
} | |
// Ensure first tab is active if none are | |
if (!activeButton && this.tabButtons.length > 0) { | |
this.tabButtons[0].classList.add('active'); | |
this.tabPanels[0].classList.add('active'); | |
this.updateSliderPosition(this.tabButtons[0]); | |
} | |
} | |
bindEvents() { | |
this.tabButtons.forEach(button => { | |
button.addEventListener('click', (e) => { | |
e.preventDefault(); | |
this.switchTab(button); | |
}); | |
}); | |
} | |
switchTab(activeButton) { | |
if (this.isAnimating) return; | |
this.isAnimating = true; | |
const targetTab = activeButton.getAttribute('data-tab'); | |
// Remove active class from all buttons and panels | |
this.tabButtons.forEach(btn => btn.classList.remove('active')); | |
this.tabPanels.forEach(panel => panel.classList.remove('active')); | |
// Add active class to clicked button and target panel | |
activeButton.classList.add('active'); | |
const targetPanel = this.container.querySelector(`#${targetTab}`); | |
if (targetPanel) { | |
targetPanel.classList.add('active'); | |
} | |
// Animate slider | |
this.updateSliderPosition(activeButton); | |
// Trigger custom event | |
this.container.dispatchEvent(new CustomEvent('tabChanged', { | |
detail: { | |
activeTab: targetTab, | |
activeButton: activeButton, | |
activePanel: targetPanel | |
} | |
})); | |
// Reset animation flag after transition | |
setTimeout(() => { | |
this.isAnimating = false; | |
}, 400); | |
} | |
updateSliderPosition(button) { | |
if (!this.tabSlider) return; | |
const buttonRect = button.getBoundingClientRect(); | |
const containerRect = this.container.getBoundingClientRect(); | |
this.tabSlider.style.width = button.offsetWidth + 'px'; | |
this.tabSlider.style.left = (button.offsetLeft - this.container.offsetLeft) + 'px'; | |
} | |
addResponsiveBehavior() { | |
// Handle window resize | |
let resizeTimeout; | |
window.addEventListener('resize', () => { | |
clearTimeout(resizeTimeout); | |
resizeTimeout = setTimeout(() => { | |
const activeButton = this.container.querySelector('.tab-button.active'); | |
if (activeButton) { | |
this.updateSliderPosition(activeButton); | |
} | |
}, 100); | |
}); | |
// Add hover effects for mobile | |
if (window.innerWidth <= 768) { | |
this.tabButtons.forEach(button => { | |
button.addEventListener('mouseenter', () => { | |
if (!button.classList.contains('active')) { | |
button.style.transform = 'scale(1.05)'; | |
} | |
}); | |
button.addEventListener('mouseleave', () => { | |
button.style.transform = 'scale(1)'; | |
}); | |
}); | |
} | |
} | |
// Public methods | |
switchToTab(tabId) { | |
const button = this.container.querySelector(`[data-tab="${tabId}"]`); | |
if (button) { | |
this.switchTab(button); | |
} | |
} | |
getActiveTab() { | |
const activeButton = this.container.querySelector('.tab-button.active'); | |
return activeButton ? activeButton.getAttribute('data-tab') : null; | |
} | |
} | |
// Auto-initialize tabs when DOM is loaded | |
document.addEventListener('DOMContentLoaded', function() { | |
const tabsContainers = document.querySelectorAll('.tabs-wrapper'); | |
tabsContainers.forEach(container => { | |
new AnimatedTabs(container); | |
}); | |
}); | |
// Export for module usage | |
if (typeof module !== 'undefined' && module.exports) { | |
module.exports = AnimatedTabs; | |
} | |