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);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc