megamenu_sdc-1.0.x-dev/components/menu_menu/menu_menu.js
components/menu_menu/menu_menu.js
(function (Drupal, once) {
// Create ONE document click handler for all instances
const documentClickHandlers = new WeakMap();
Drupal.behaviors.menuMenu = {
attach(context) {
once('menuMenu', '[data-component-id="megamenu_sdc:menu_menu"]', context).forEach((menuComponent) => {
const detailsElements = menuComponent.querySelectorAll(':scope > li > details');
let activeDetails = null; // Semaphore for clicked/active menu
let hoverDetails = null; // Track currently hovered menu
let hoverTimeout = null; // Track timeouts for hover events
// Add document click handler ONCE per menu component
const documentClickHandler = (event) => {
// If we have an active menu and clicked outside the menu component
if (activeDetails && !menuComponent.contains(event.target)) {
activeDetails._programmaticToggle = true;
activeDetails.removeAttribute('open');
activeDetails.setAttribute('data-menu-state', 'closed');
activeDetails._programmaticToggle = false;
activeDetails = null;
}
};
// Only add the handler if it doesn't exist
if (!documentClickHandlers.has(menuComponent)) {
document.addEventListener('click', documentClickHandler);
documentClickHandlers.set(menuComponent, documentClickHandler);
}
// Store the handler reference for potential cleanup
menuComponent._documentClickHandler = documentClickHandler;
detailsElements.forEach(details => {
// Add data attribute to track state
details.setAttribute('data-menu-state', 'closed');
// Handle mouse enter in desktop mode
details.addEventListener('mouseenter', () => {
// Only handle hover if in desktop mode
if (!document.body.classList.contains('menu-pane-desktop')) {
return;
}
// If we have an active menu from clicking, don't interfere with it
if (activeDetails) {
return;
}
// Clear any pending hover timeout
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
// Get menu level from the component containing this details element
const currentMenuLevel = parseInt(menuComponent.dataset.menuLevel || '1', 10);
// Find the closest parent details that might be from a different menu level
const closestParentDetails = details.closest('[data-component-id="megamenu_sdc:menu_menu"] details');
const isNestedMenu = closestParentDetails && closestParentDetails !== details;
// Handle hover interactions with other menu items
if (hoverDetails && hoverDetails !== details) {
// Determine if we need to close the previously hovered menu
let shouldCloseHovered = true;
// Don't close if it's a parent of current menu
if (details.closest('details') === hoverDetails) {
shouldCloseHovered = false;
}
// Don't close level 1 items when hovering level 2+ items
const hoveredMenuComponent = hoverDetails.closest('[data-component-id="megamenu_sdc:menu_menu"]');
if (hoveredMenuComponent) {
const hoveredLevel = parseInt(hoveredMenuComponent.dataset.menuLevel || '1', 10);
if (hoveredLevel === 1 && currentMenuLevel > 1) {
shouldCloseHovered = false;
}
}
// Close previous hover if needed
if (shouldCloseHovered) {
// Check if the currently hovered item is at the same level
const hoveredDetailsInSameMenu = menuComponent.contains(hoverDetails);
if (hoveredDetailsInSameMenu) {
hoverDetails._programmaticToggle = true;
hoverDetails.removeAttribute('open');
hoverDetails.setAttribute('data-menu-state', 'closed');
hoverDetails._programmaticToggle = false;
}
}
}
// Open this menu with hover state
details._programmaticToggle = true;
details.setAttribute('open', '');
details.setAttribute('data-menu-state', 'hover');
hoverDetails = details;
details._programmaticToggle = false;
});
details.addEventListener('mouseleave', () => {
// Only close on mouse leave if this menu was opened by hover
if (document.body.classList.contains('menu-pane-desktop') &&
details.getAttribute('data-menu-state') === 'hover') {
// Set timeout to give user time to move to submenu
hoverTimeout = setTimeout(() => {
// Double-check state before closing
if (details === hoverDetails &&
details.getAttribute('data-menu-state') === 'hover') {
details._programmaticToggle = true;
details.removeAttribute('open');
details.setAttribute('data-menu-state', 'closed');
details._programmaticToggle = false;
hoverDetails = null;
}
hoverTimeout = null;
}, 300); // 300ms delay before closing hover menu
}
});
// Prevent default details toggle behavior
details.addEventListener('toggle', (event) => {
// Only prevent default if this wasn't triggered by our code
if (!details._programmaticToggle) {
event.preventDefault();
event.stopPropagation();
}
}, true);
const summary = details.querySelector('summary');
if (summary) {
const link = summary.querySelector('a');
if (link) {
// Use mouseup event for links which happens before click but after mousedown
link.addEventListener('mouseup', (event) => {
// Allow the click to pass through naturally without being intercepted
event.stopPropagation();
// If it's a normal left-click without modifier keys, force navigation
if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
const href = link.getAttribute('href');
if (href) {
event.preventDefault(); // Prevent any default behavior
window.location.href = href; // Force navigation
}
}
});
}
// Prevent browser toggle on mousedown/touchstart
['mousedown', 'touchstart'].forEach(eventType => {
summary.addEventListener(eventType, (event) => {
// If the click is directly on the link or its children, let it pass through
if (link && (event.target === link || link.contains(event.target))) {
// Let the event continue for links
return;
}
// Otherwise prevent the default browser toggle
event.preventDefault();
event.stopPropagation();
}, { passive: false });
});
// Handle click for dropdown toggling
summary.addEventListener('click', (event) => {
// If the click is directly on the link or its children, let it pass through
if (link && (event.target === link || link.contains(event.target))) {
return;
}
// Otherwise, handle as dropdown toggle
event.preventDefault();
event.stopPropagation();
// Clear any pending hover timeout
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
// Reset hover tracking
if (hoverDetails) {
if (hoverDetails !== details) {
hoverDetails._programmaticToggle = true;
hoverDetails.removeAttribute('open');
hoverDetails.setAttribute('data-menu-state', 'closed');
hoverDetails._programmaticToggle = false;
}
hoverDetails = null;
}
// Handle the semaphore logic for active menus
if (activeDetails && activeDetails !== details) {
// Close the previous active menu
activeDetails._programmaticToggle = true;
activeDetails.removeAttribute('open');
activeDetails.setAttribute('data-menu-state', 'closed');
activeDetails._programmaticToggle = false;
}
// Toggle current details
details._programmaticToggle = true;
if (details.hasAttribute('open') &&
details.getAttribute('data-menu-state') === 'active') {
// If this menu is already active, close it
details.removeAttribute('open');
details.setAttribute('data-menu-state', 'closed');
activeDetails = null;
} else {
// Otherwise, open and set as active
details.setAttribute('open', '');
details.setAttribute('data-menu-state', 'active');
activeDetails = details;
}
details._programmaticToggle = false;
});
// Handle touch events similarly
if ('ontouchend' in window) {
summary.addEventListener('touchend', (event) => {
// If the touch is directly on the link or its children, let it pass through
if (link && (event.target === link || link.contains(event.target))) {
return;
}
// Otherwise, handle as dropdown toggle
event.preventDefault();
// Simulate click for consistent behavior
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
summary.dispatchEvent(clickEvent);
});
}
}
});
// Clean up handler when component is removed
menuComponent._cleanup = () => {
const handler = documentClickHandlers.get(menuComponent);
if (handler) {
document.removeEventListener('click', handler);
documentClickHandlers.delete(menuComponent);
}
};
});
}
};
})(Drupal, once);
