// Configuration object for centralized settings
const TabbedNavConfig = {
DESKTOP_BREAKPOINT: 1201,
DEBOUNCE_DELAY: 250,
OPEN_FIRST_TAB: true,
CLOSE_TAB_ON_MOUSEOUT: false
};
// Custom config for tabbed hero containers
const tabbedHeroConfig = {
...TabbedNavConfig,
OPEN_FIRST_TAB: false,
CLOSE_TAB_ON_MOUSEOUT: true
};
// Function to determine the appropriate config based on container context
function getConfigForContainer(container) {
// Check if container has closest ancestor with [data-tabbed-hero="true"]
const heroAncestor = container.getAttribute('data-tabbed-hero') === 'true' ? container : container.closest('[data-tabbed-dropdown="true"]');
if (heroAncestor) {
return tabbedHeroConfig;
}
// Check for specific data-instance attributes (if you still want to support them)
// const instance = container.getAttribute('data-instance');
// if (instance === 'two') {
// // You can still have specific instance configs if needed
// return {
// ...TabbedNavConfig,
// OPEN_FIRST_TAB: false,
// CLOSE_TAB_ON_MOUSEOUT: true
// };
// }
// Default config
return TabbedNavConfig;
}
class TabbedNavigation {
constructor(container, config = TabbedNavConfig) {
this.container = container;
this.config = config;
this.activeItem = null;
this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT;
this.hasMouseListeners = false;
this.megamenuParent = null;
this.megamenuObserver = null;
// Initialize bound handlers - these will be reused across reinitializations
this.mouseEnterHandler = this.handleMouseEnter.bind(this);
this.mouseLeaveHandler = this.handleMouseLeave.bind(this);
this.resizeHandler = this.debounce(this.handleResize.bind(this), this.config.DEBOUNCE_DELAY);
this.handleMegamenuToggle = this.handleMegamenuToggle.bind(this);
this.init();
}
init() {
// Cache DOM elements fresh each time
this.navItems = [...this.container.querySelectorAll('.dwc-tabbed-nav-list__li')];
this.elementsCache = new Map();
this.cacheElements();
this.setupAccessibility();
this.setupMegamenuObserver();
this.bindEvents();
// Set initial state
this.updateLayout();
this.setBackText()
// Show first item on page load only if desktop, not nested, and OPEN_FIRST_TAB is true
if (this.navItems.length > 0 && this.isDesktop && this.config.OPEN_FIRST_TAB) {
setTimeout(() => {
const firstItem = this.navItems[0];
// Count how many .dwc-tabbed-nav-container ancestors this.container has
let count = 0;
let current = this.container.parentElement;
while (current) {
if (current.classList?.contains('dwc-tabbed-nav-container')) {
count++;
}
current = current.parentElement;
}
const isNested = count >= 1;
if (!isNested && this.elementsCache.has(firstItem)) {
this.showTab(firstItem);
}
}, 0);
}
}
// Setup observer for megamenu parent
setupMegamenuObserver() {
// Find the .brx-has-megamenu parent
this.megamenuParent = this.container.closest('.brx-has-megamenu');
if (this.megamenuParent) {
// Set initial inert state
this.updateInertState();
// Create a MutationObserver to watch for class changes
this.megamenuObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
this.updateInertState();
}
});
});
// Start observing the megamenu parent for class changes
this.megamenuObserver.observe(this.megamenuParent, {
attributes: true,
attributeFilter: ['class']
});
}
}
// Update inert state based on megamenu open/closed state
updateInertState() {
if (!this.megamenuParent) return;
const isOpen = this.megamenuParent.classList.contains('open');
if (isOpen) {
this.container.removeAttribute('inert');
} else {
this.container.setAttribute('inert', '');
}
}
// Handle megamenu toggle (for manual triggering if needed)
handleMegamenuToggle() {
this.updateInertState();
}
// Method to clean up event listeners and reset state
cleanup() {
// Remove all event listeners
this.container.removeEventListener('click', this.handleClick);
this.container.removeEventListener('keydown', this.handleKeydown);
this.container.removeEventListener('focus', this.handleFocus);
// Remove mouse listeners if they exist
if (this.hasMouseListeners) {
this.container.removeEventListener('mouseenter', this.mouseEnterHandler, true);
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler, true);
}
// Remove resize listener
window.removeEventListener('resize', this.resizeHandler);
// Clean up megamenu observer
if (this.megamenuObserver) {
this.megamenuObserver.disconnect();
this.megamenuObserver = null;
}
// Reset state
this.activeItem = null;
this.hasMouseListeners = false;
this.megamenuParent = null;
// Clear caches
if (this.elementsCache) {
this.elementsCache.clear();
}
this.navItems = [];
// Reset all tab states and accessibility attributes
const allNavItems = this.container.querySelectorAll('.dwc-tabbed-nav-list__li');
allNavItems.forEach(item => {
const button = item.querySelector('.dwc-tabbed-nav-list__li__button') ||
item.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
const content = item.querySelector('.dwc-tabbed-nav-list__li__content');
// Remove classes
item.classList.remove('active');
// Reset button attributes
if (button) {
button.removeAttribute('role');
button.removeAttribute('aria-controls');
button.removeAttribute('aria-expanded');
button.removeAttribute('aria-selected');
button.removeAttribute('tabindex');
button.removeAttribute('id');
}
// Reset content attributes
if (content) {
content.removeAttribute('role');
content.removeAttribute('aria-labelledby');
content.removeAttribute('id');
content.removeAttribute('aria-hidden');
content.style.maxHeight = '';
}
// Remove data attributes
item.removeAttribute('data-tab-index');
});
// Reset nav list role
const navList = this.container.querySelector('.dwc-tabbed-nav-list');
if (navList) {
navList.removeAttribute('role');
}
// Remove inert attribute
this.container.removeAttribute('inert');
}
// Public method to reinitialize the navigation
reinitialize() {
this.cleanup();
this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT; // Use config breakpoint
this.init();
}
// Cache DOM elements to avoid repeated queries
cacheElements() {
const uniqueIdBase = `tab-${Date.now()}`;
this.navItems.forEach((item, index) => {
const button = item.querySelector('.dwc-tabbed-nav-list__li__button') ||
item.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
const content = item.querySelector('.dwc-tabbed-nav-list__li__content');
this.elementsCache.set(item, {
button,
content,
uniqueId: `${uniqueIdBase}-${index}`,
index
});
});
}
setupAccessibility() {
const navList = this.container.querySelector('.dwc-tabbed-nav-list');
if (!navList) return;
navList.setAttribute('role', 'tablist');
for (const [item, cache] of this.elementsCache) {
const { button, content, uniqueId, index } = cache;
if (button) {
button.setAttribute('role', 'tab');
button.setAttribute('aria-controls', `${uniqueId}-panel`);
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-selected', 'false');
button.setAttribute('tabindex', '0');
button.setAttribute('id', `${uniqueId}-tab`);
}
if (content) {
content.setAttribute('role', 'tabpanel');
content.setAttribute('aria-labelledby', `${uniqueId}-tab`);
content.setAttribute('id', `${uniqueId}-panel`);
content.setAttribute('aria-hidden', 'true');
}
item.setAttribute('data-tab-index', index);
}
}
bindEvents() {
// Store bound handlers for cleanup
this.handleClick = this.handleClick.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.handleFocus = this.handleFocus.bind(this);
// Single event delegation for better performance
this.container.addEventListener('click', this.handleClick);
this.container.addEventListener('keydown', this.handleKeydown);
this.container.addEventListener('focus', this.handleFocus, true);
// Passive resize listener for better performance
window.addEventListener('resize', this.resizeHandler, { passive: true });
this.updateMouseListeners();
}
updateMouseListeners() {
if (this.isDesktop && !this.hasMouseListeners) {
this.container.addEventListener('mouseenter', this.mouseEnterHandler, true);
this.container.addEventListener('mouseleave', this.mouseLeaveHandler, true);
this.hasMouseListeners = true;
} else if (!this.isDesktop && this.hasMouseListeners) {
this.container.removeEventListener('mouseenter', this.mouseEnterHandler, true);
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler, true);
this.hasMouseListeners = false;
}
}
// Helper method to get sibling nav items (nav items in the same container level)
getSiblingNavItems(navItem) {
// Find the direct parent container of this nav item
const navItemContainer = navItem.closest('.dwc-tabbed-nav-list');
if (!navItemContainer) return [];
// Get all nav items that are direct children of the same container
const siblingItems = [...navItemContainer.querySelectorAll('.dwc-tabbed-nav-list__li')];
// Filter to only include items that are siblings (not the navItem itself)
// and check if they belong to any TabbedNavigation instance
return siblingItems.filter(item => item !== navItem);
}
handleFocus(e) {
if (!this.isDesktop) return;
const button = e.target.closest('[role="tab"]');
if (!button) return;
const navItem = button.closest('.dwc-tabbed-nav-list__li');
if (!navItem || !this.elementsCache.has(navItem)) return;
this.handleTabInteraction(navItem, 'focus');
}
handleClick(e) {
const navItem = e.target.closest('.dwc-tabbed-nav-list__li');
if (!navItem || !this.elementsCache.has(navItem)) return;
const button = e.target.closest('.dwc-tabbed-nav-list__li__button, .dwc-tabbed-nav-list__li__btn-txt');
if (!button) return;
e.preventDefault();
this.handleTabInteraction(navItem, 'click');
}
handleKeydown(e) {
const button = e.target.closest('[role="tab"]');
if (!button) return;
const navItem = button.closest('.dwc-tabbed-nav-list__li');
if (!navItem || !this.elementsCache.has(navItem)) return;
const { index: currentIndex } = this.elementsCache.get(navItem);
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
this.handleTabInteraction(navItem, 'keyboard');
break;
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
this.focusNextTab(currentIndex);
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
this.focusPrevTab(currentIndex);
break;
case 'Home':
e.preventDefault();
this.focusTab(0);
break;
case 'End':
e.preventDefault();
this.focusTab(this.navItems.length - 1);
break;
}
}
handleMouseEnter(e) {
if (!this.isDesktop) return;
const navItem = e.target.closest('.dwc-tabbed-nav-list__li') ||
(e.target.matches('.dwc-tabbed-nav-list__li') ? e.target : null);
if (!navItem || !this.elementsCache.has(navItem)) return;
this.handleTabInteraction(navItem, 'hover');
}
handleMouseLeave(e) {
if (!this.isDesktop) return;
// Only close tabs if CLOSE_TAB_ON_MOUSEOUT is enabled
if (this.config.CLOSE_TAB_ON_MOUSEOUT) {
// Check if mouse is leaving the entire container
const rect = this.container.getBoundingClientRect();
const { clientX, clientY } = e;
// Check if mouse position is outside the container bounds
const isOutsideContainer = (
clientX < rect.left ||
clientX > rect.right ||
clientY < rect.top ||
clientY > rect.bottom
);
if (isOutsideContainer) {
this.hideAllTabs();
}
}
// If CLOSE_TAB_ON_MOUSEOUT is false, keep content visible (original behavior)
}
// Unified method to handle all tab interactions with nesting support
handleTabInteraction(navItem, interactionType) {
if (!this.elementsCache.has(navItem)) return;
if (this.isDesktop) {
// Simply hide siblings and show the target - works for all scenarios
this.hideSiblingTabs(navItem);
this.showTab(navItem);
} else {
// Mobile accordion behavior
if (interactionType === 'click' || interactionType === 'keyboard') {
this.toggleTabMobile(navItem);
}
}
}
// Hide only sibling tabs (same container level)
hideSiblingTabs(navItem) {
// console.log('Nav Item: ', navItem)
const siblings = this.getSiblingNavItems(navItem);
siblings.forEach(sibling => {
//console.log('sibling: ', sibling)
// Only hide siblings that have the active class to avoid affecting other instances
if (sibling.classList.contains('active')) {
this.hideSiblingTab(sibling);
}
});
}
// Hide a sibling tab (used when we know it's a sibling from another instance)
hideSiblingTab(navItem) {
const button = navItem.querySelector('.dwc-tabbed-nav-list__li__button') ||
navItem.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
const content = navItem.querySelector('.dwc-tabbed-nav-list__li__content');
if (content && button) {
content.setAttribute('aria-hidden', 'true');
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-selected', 'false');
navItem.classList.remove('active');
// Also handle mobile accordion collapse
if (!this.isDesktop) {
content.style.maxHeight = '0';
}
}
}
// Mobile-specific toggle behavior with proper nesting support
toggleTabMobile(navItem) {
if (!this.elementsCache.has(navItem)) return;
const isActive = navItem.classList.contains('active');
if (isActive) {
this.collapseTab(navItem);
} else {
// Close sibling tabs and open this one
this.hideSiblingTabs(navItem);
this.expandTab(navItem);
}
}
toggleTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
if (this.isDesktop) {
this.handleTabInteraction(navItem, 'click');
} else {
this.toggleTabMobile(navItem);
}
}
showTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
const { button, content } = this.elementsCache.get(navItem);
if (content && button) {
content.setAttribute('aria-hidden', 'false');
button.setAttribute('aria-expanded', 'true');
button.setAttribute('aria-selected', 'true');
navItem.classList.add('active');
}
this.setActiveTab(navItem);
}
hideTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
const { button, content } = this.elementsCache.get(navItem);
if (content && button) {
content.setAttribute('aria-hidden', 'true');
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-selected', 'false');
navItem.classList.remove('active');
}
// Only clear activeItem if it belongs to THIS navigation instance
// and it's the exact item being hidden
if (this.activeItem === navItem && this.navItems.includes(navItem)) {
this.activeItem = null;
}
}
expandTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
// Close sibling tabs
this.hideSiblingTabs(navItem);
this.showTab(navItem);
// Smooth height animation
const { content } = this.elementsCache.get(navItem);
if (content) {
const height = content.scrollHeight;
content.style.maxHeight = `${height}px`;
}
}
collapseTab(navItem) {
if (!this.elementsCache.has(navItem)) return;
this.hideTab(navItem);
const { content } = this.elementsCache.get(navItem);
if (content) {
content.style.maxHeight = '0';
}
}
hideAllTabs() {
this.navItems.forEach(item => this.hideTab(item));
}
setActiveTab(navItem) {
// Only update aria-selected for tabs in THIS navigation instance
this.navItems.forEach(item => {
const cache = this.elementsCache.get(item);
if (cache?.button) {
cache.button.setAttribute('aria-selected', item === navItem ? 'true' : 'false');
}
});
// Only set activeItem if this navItem belongs to THIS navigation instance
if (this.navItems.includes(navItem)) {
this.activeItem = navItem;
}
}
focusTab(index) {
if (index >= 0 && index < this.navItems.length) {
const cache = this.elementsCache.get(this.navItems[index]);
if (cache?.button) {
cache.button.focus();
}
}
}
focusNextTab(currentIndex) {
const nextIndex = (currentIndex + 1) % this.navItems.length;
this.focusTab(nextIndex);
}
focusPrevTab(currentIndex) {
const prevIndex = currentIndex === 0 ? this.navItems.length - 1 : currentIndex - 1;
this.focusTab(prevIndex);
}
handleResize() {
const wasDesktop = this.isDesktop;
this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT; // Use config breakpoint
if (wasDesktop !== this.isDesktop) {
this.updateMouseListeners();
this.updateLayout();
}
}
updateLayout() {
const currentlyActive = this.activeItem;
// Reset all states
this.hideAllTabs();
// Clear inline styles
this.navItems.forEach(item => {
const cache = this.elementsCache.get(item);
if (cache?.content) {
cache.content.style.maxHeight = '';
}
});
if (this.isDesktop) {
// Determine if this tab instance is nested
let count = 0;
let current = this.container.parentElement;
while (current) {
if (current.classList?.contains('dwc-tabbed-nav-container')) {
count++;
}
current = current.parentElement;
}
const isNested = count >= 1;
if (!isNested && this.config.OPEN_FIRST_TAB) {
if (currentlyActive) {
this.showTab(currentlyActive);
} else if (this.navItems.length > 0) {
this.showTab(this.navItems[0]);
}
}
}
}
setBackText(){
let tabElement = document.querySelector('.dwc-tabbed-nav-container');
let tabBackText = tabElement.getAttribute('data-back-text')
document.querySelectorAll('.dwc-tabbed-nav-list__li__button').forEach(tabBtn => {
const textContent = tabBtn.textContent.trim();
const tabIcon = tabBtn.querySelector('.dwc-tabbed-nav-list__li__arrow-icon');
if (tabIcon) {
tabIcon.setAttribute('data-text', textContent);
tabIcon.setAttribute('data-back-text', tabBackText);
}
});
}
// Simple debounce for resize
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Public method to destroy the instance completely
destroy() {
this.cleanup();
// Additional cleanup can be added here if needed
}
// Public method to manually update inert state (if needed)
updateInert() {
this.updateInertState();
}
}
// Simplified initialization on DOM ready
document.addEventListener('DOMContentLoaded', function() {
const tabbedNavContainers = document.querySelectorAll('.dwc-tabbed-nav-container');
tabbedNavContainers.forEach(container => {
// Skip if this container is nested inside another dwc-tabbed-nav-container
const parentNavContainer = container.parentElement.closest('.dwc-tabbed-nav-container');
if (parentNavContainer) {
return; // Skip nested containers
}
// Get the appropriate config for this container
const config = getConfigForContainer(container);
// Initialize with the determined config
new TabbedNavigation(container, config);
});
});
// For dynamic content - Updated version
window.initTabbedNav = function(container, customConfig = null) {
if (container && container.classList.contains('dwc-tabbed-nav-container')) {
// Check if this is a nested container
const parentNavContainer = container.parentElement.closest('.dwc-tabbed-nav-container');
if (parentNavContainer) {
return null; // Don't initialize nested containers
}
// Use custom config if provided, otherwise determine config automatically
const config = customConfig || getConfigForContainer(container);
return new TabbedNavigation(container, config);
}
return null;
};
/*
// TO REINITIALIZE USE THIS
// Create an instance of TabbedNavigation
const tabbedNav = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'));
// Call reinitialize on the instance
tabbedNav.reinitialize();
// TO USE WITH CUSTOM CONFIG
const customConfig = {
DESKTOP_BREAKPOINT: 768,
DEBOUNCE_DELAY: 300,
OPEN_FIRST_TAB: false, // This will prevent first tab from opening automatically
CLOSE_TAB_ON_MOUSEOUT: true // This will close tabs when mouse leaves container
};
const tabbedNavCustom = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'), customConfig);
// TO UPDATE ON URL CHANGE
function onUrlChange() {
setTimeout(function () {
const tabbedNav = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'));
tabbedNav.reinitialize();
}, 1500); // Delay execution by 2 seconds
}
// Listen for popstate event (history navigation)
window.addEventListener('popstate', onUrlChange);
// Listen for hashchange event (URL hash change)
window.addEventListener('hashchange', onUrlChange);
// Handle pushState and replaceState
(function (history) {
const pushState = history.pushState;
const replaceState = history.replaceState;
history.pushState = function (state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({ state: state });
}
const result = pushState.apply(history, arguments);
onUrlChange(); // Call the function when pushState is used
return result;
};
history.replaceState = function (state) {
if (typeof history.onreplacestate == "function") {
history.onreplacestate({ state: state });
}
const result = replaceState.apply(history, arguments);
onUrlChange(); // Call the function when replaceState is used
return result;
};
})(window.history);
*/
console.log('%c<Jamie Sheehan Web Design Mega Menu Pro Tabbed Navigation v1.0>', 'color: #b388eb');