/** * TAE Header - Main JavaScript (Refactored) * Simplified and optimized following KISS & DRY principles */ (function () { 'use strict'; // ============================================================================ // CONFIGURATION // ============================================================================ const CONFIG = { scroll: { threshold: 50, transitionDelay: 200, throttleDelay: 100, resizeThrottle: 200 }, mobile: { breakpoint: 991, closeDelay: 400, resizeDebounce: 250 }, selectors: { header: '.header', headerWrapper: '.header__row', submenus: '.tae-item-submenu-level-1', // The drawer can be theme-specific or our own drawer: '.mobile-menu-drawer, [ref="mobileMenuDrawer"], .tae-mobile-menu-drawer', // Internal navigation elements breadcrumb: '[data-mobile-menu-breadcrumb]', breadcrumbTitle: '[data-mobile-menu-breadcrumb-title]', backBtn: '[data-mobile-menu-back]', navigation: '[data-mobile-menu-navigation]', activeSubmenus: '.tae-mobile-submenu.is-active' }, classes: { scrolled: 'is-scrolled', menuOpen: 'mobile-menu-open', active: 'is-active', open: 'is-open' } }; // ============================================================================ // UTILITIES MODULE // ============================================================================ const Utils = { /** * Throttle function execution */ throttle(func, limit) { let inThrottle; return function (...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }, /** * Debounce function execution */ debounce(func, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }, /** * Check if viewport is mobile */ isMobile() { return window.innerWidth <= CONFIG.mobile.breakpoint; }, /** * Get scroll position */ getScrollTop() { return window.pageYOffset || document.documentElement.scrollTop; }, /** * Query single element safely */ query(selector, context = document) { try { return context.querySelector(selector); } catch (error) { console.warn(`TAE Header: Invalid selector "${selector}"`, error); return null; } }, /** * Query multiple elements safely */ queryAll(selector, context = document) { try { return context.querySelectorAll(selector); } catch (error) { console.warn(`TAE Header: Invalid selector "${selector}"`, error); return []; } }, /** * Check if in Shopify design mode */ isDesignMode() { return typeof window.Shopify !== 'undefined' && window.Shopify.designMode; } }; // ============================================================================ // SCROLL BEHAVIOR MODULE // ============================================================================ const ScrollBehavior = { state: { isScrolled: false, cachedHeaderHeight: 0, ticking: false }, elements: { header: null, headerWrapper: null, submenus: null }, /** * Initialize scroll behavior */ init() { this.cacheElements(); this.updateState(); this.bindEvents(); }, /** * Cache DOM elements */ cacheElements() { this.elements.header = Utils.query(CONFIG.selectors.header); this.elements.headerWrapper = Utils.query(CONFIG.selectors.headerWrapper); this.elements.submenus = Utils.queryAll(CONFIG.selectors.submenus); }, /** * Update scroll state */ updateState() { const scrollTop = Utils.getScrollTop(); const shouldBeScrolled = scrollTop > CONFIG.scroll.threshold; if (shouldBeScrolled !== this.state.isScrolled) { this.state.isScrolled = shouldBeScrolled; document.body.classList.toggle(CONFIG.classes.scrolled, shouldBeScrolled); setTimeout(() => this.updateHeaderHeight(), CONFIG.scroll.transitionDelay); } }, /** * Update header height and position submenus */ updateHeaderHeight() { if (!this.elements.headerWrapper) return; const headerHeight = this.elements.headerWrapper.offsetHeight; if (headerHeight === this.state.cachedHeaderHeight) return; this.state.cachedHeaderHeight = headerHeight; document.documentElement.style.setProperty('--header-height', `${headerHeight}px`); this.elements.submenus.forEach(submenu => { submenu.style.top = `${headerHeight}px`; }); }, /** * Handle scroll with RAF optimization */ handleScroll() { if (!this.state.ticking) { window.requestAnimationFrame(() => { this.updateState(); this.state.ticking = false; }); this.state.ticking = true; } }, /** * Handle transition end */ handleTransitionEnd(event) { if (event.target === this.elements.header || event.target.closest(CONFIG.selectors.header)) { this.updateHeaderHeight(); } }, /** * Bind event listeners */ bindEvents() { const boundScroll = this.handleScroll.bind(this); const boundResize = Utils.throttle(() => this.updateState(), CONFIG.scroll.resizeThrottle); window.addEventListener('scroll', boundScroll, { passive: true }); window.addEventListener('resize', boundResize, { passive: true }); if (this.elements.header) { this.elements.header.addEventListener('transitionend', this.handleTransitionEnd.bind(this)); } // Cleanup for Shopify Theme Editor if (Utils.isDesignMode()) { document.addEventListener('shopify:section:unload', () => { window.removeEventListener('scroll', boundScroll); window.removeEventListener('resize', boundResize); }); } } }; // ============================================================================ // MOBILE MENU MODULE // ============================================================================ const MobileMenu = { state: { initialized: false, menuStack: [], isDrawerOpen: false, drawerObserver: null, bodyObserver: null }, elements: {}, /** * Initialize mobile menu */ init() { if (!Utils.isMobile()) { this.bindResizeListener(); return; } this.setup(); }, /** * Setup mobile menu */ setup() { if (this.state.initialized) return; this.cacheElements(); if (!this.validateElements()) { console.warn('TAE Header: Mobile menu initialization failed - required elements not found'); return; } this.state.initialized = true; window.taeHeaderMobileMenuInitialized = true; this.bindEvents(); }, /** * Cache DOM elements */ cacheElements() { const { selectors } = CONFIG; this.elements = { hamburger: Utils.query(selectors.hamburger), drawer: Utils.query(selectors.drawer), closeBtn: Utils.query(selectors.closeBtn), overlay: Utils.query(selectors.overlay), breadcrumb: Utils.query(selectors.breadcrumb), breadcrumbTitle: Utils.query(selectors.breadcrumbTitle), backBtn: Utils.query(selectors.backBtn), navigation: Utils.query(selectors.navigation) }; }, /** * Validate required elements exist */ validateElements() { const required = ['drawer', 'navigation']; return required.every(key => this.elements[key] !== null); }, /** * Open menu (Handled by Horizon theme) */ open() { // No-op: Horizon theme handles drawer open }, /** * Close menu (Handled by Horizon theme) */ close() { // No-op: Horizon theme handles drawer close }, /** * Reset to main menu */ reset() { if (this.elements.breadcrumb) { this.elements.breadcrumb.style.display = 'none'; } if (this.elements.breadcrumbTitle) { this.elements.breadcrumbTitle.textContent = ''; } const activeSubmenus = Utils.queryAll(CONFIG.selectors.activeSubmenus); activeSubmenus.forEach(submenu => { submenu.classList.remove(CONFIG.classes.active); submenu.style.display = 'none'; }); // Restore all hidden sibling items const allItems = Utils.queryAll('.tae-mobile-menu-item'); allItems.forEach(item => { item.style.display = ''; }); this.state.menuStack = []; }, /** * Check if the mobile drawer is currently open */ isDrawerOpen() { const drawerIsOpen = this.elements.drawer?.classList.contains(CONFIG.classes.open); const bodyHasMenuOpen = document.body.classList.contains(CONFIG.classes.menuOpen); return Boolean(drawerIsOpen || bodyHasMenuOpen); }, /** * Synchronize drawer state to reset navigation when closed */ syncDrawerState() { const isOpen = this.isDrawerOpen(); if (!isOpen && this.state.isDrawerOpen) { this.reset(); } if (isOpen && !this.state.isDrawerOpen) { this.reset(); } this.state.isDrawerOpen = isOpen; }, /** * Navigate to submenu */ navigateToSubmenu(trigger) { const submenuId = trigger.dataset.submenuTrigger; if (!submenuId) { console.warn('TAE Header: Missing submenu trigger ID'); return; } const submenu = Utils.query(`[data-submenu="${submenuId}"]`); if (!submenu) { console.warn('TAE Header: Submenu not found:', submenuId); return; } // Hide all currently active submenus before showing the new one // BUT skip any that are ancestors of the target submenu (nested structure) const activeSubmenus = Utils.queryAll(CONFIG.selectors.activeSubmenus); activeSubmenus.forEach(s => { if (!s.contains(submenu)) { s.classList.remove(CONFIG.classes.active); s.style.display = 'none'; } }); // Hide sibling items in the parent list (only show the target submenu's content) const parentListItem = trigger.closest('.tae-mobile-menu-item'); const parentList = trigger.closest('.tae-mobile-menu-list, .tae-mobile-submenu'); if (parentList) { const siblings = parentList.querySelectorAll(':scope > .tae-mobile-menu-item'); siblings.forEach(sibling => { if (sibling !== parentListItem) { sibling.style.display = 'none'; } }); } const parentTitle = submenu.dataset.parentTitle || 'Menu'; this.state.menuStack.push({ submenu, title: parentTitle, parentList, parentListItem }); if (this.elements.breadcrumb && this.elements.breadcrumbTitle) { this.elements.breadcrumb.style.display = 'flex'; this.elements.breadcrumbTitle.textContent = parentTitle; } submenu.classList.add(CONFIG.classes.active); submenu.style.display = 'block'; }, /** * Navigate back */ goBack() { if (this.state.menuStack.length === 0) return; const current = this.state.menuStack.pop(); current.submenu.classList.remove(CONFIG.classes.active); current.submenu.style.display = 'none'; // Restore sibling items that were hidden when navigating deeper if (current.parentList) { const siblings = current.parentList.querySelectorAll(':scope > .tae-mobile-menu-item'); siblings.forEach(sibling => { sibling.style.display = ''; }); } if (this.state.menuStack.length === 0) { if (this.elements.breadcrumb) { this.elements.breadcrumb.style.display = 'none'; } } else { // Show the previous submenu in the stack const previous = this.state.menuStack[this.state.menuStack.length - 1]; previous.submenu.classList.add(CONFIG.classes.active); previous.submenu.style.display = 'block'; if (this.elements.breadcrumbTitle) { this.elements.breadcrumbTitle.textContent = previous.title; } } }, /** * Handle submenu click (event delegation) */ handleSubmenuClick(event) { const trigger = event.target.closest('[data-submenu-trigger]'); if (trigger) { event.preventDefault(); this.navigateToSubmenu(trigger); } }, /** * Handle keyboard events */ handleKeydown(event) { if (event.key === 'Escape' && this.elements.drawer?.classList.contains(CONFIG.classes.open)) { this.close(); } }, /** * Handle window resize */ handleResize() { if (window.innerWidth > CONFIG.mobile.breakpoint && this.elements.drawer?.classList.contains(CONFIG.classes.open)) { this.close(); } }, /** * Bind event listeners */ bindEvents() { const { elements } = this; elements.hamburger?.addEventListener('click', () => this.open()); elements.closeBtn?.addEventListener('click', () => this.close()); elements.overlay?.addEventListener('click', () => this.close()); elements.backBtn?.addEventListener('click', () => this.goBack()); elements.navigation?.addEventListener('click', (e) => this.handleSubmenuClick(e)); document.addEventListener('keydown', (e) => this.handleKeydown(e)); const debouncedResize = Utils.debounce(() => this.handleResize(), CONFIG.mobile.resizeDebounce); window.addEventListener('resize', debouncedResize); this.bindDrawerStateObserver(); this.syncDrawerState(); // Cleanup for Shopify Theme Editor if (Utils.isDesignMode()) { document.addEventListener('shopify:section:unload', () => { this.state.drawerObserver?.disconnect(); this.state.bodyObserver?.disconnect(); this.state.drawerObserver = null; this.state.bodyObserver = null; this.state.initialized = false; this.state.isDrawerOpen = false; window.taeHeaderMobileMenuInitialized = false; }); } }, /** * Observe drawer/body state changes handled by the theme */ bindDrawerStateObserver() { if (typeof MutationObserver === 'undefined') return; this.state.drawerObserver?.disconnect(); this.state.bodyObserver?.disconnect(); if (this.elements.drawer) { this.state.drawerObserver = new MutationObserver(() => this.syncDrawerState()); this.state.drawerObserver.observe(this.elements.drawer, { attributes: true, attributeFilter: ['class'] }); } this.state.bodyObserver = new MutationObserver(() => this.syncDrawerState()); this.state.bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); }, /** * Bind resize listener for lazy initialization */ bindResizeListener() { const debouncedInit = Utils.debounce(() => { if (Utils.isMobile() && !this.state.initialized) { this.setup(); } }, CONFIG.mobile.resizeDebounce); window.addEventListener('resize', debouncedInit); } }; // ============================================================================ // INITIALIZATION // ============================================================================ function initialize() { ScrollBehavior.init(); MobileMenu.init(); } // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();