| 
1 | 1 | /**  | 
2 |  | - * Toggles the user dropdown menu visibility and manages event listeners.  | 
3 |  | - * Handles click-outside-to-close functionality.  | 
 | 2 | + * Initializes header dropdown behavior with HTMX awareness.  | 
4 | 3 |  */  | 
5 |  | -export const onClickDropdown = () => {  | 
6 |  | -  const dropdownButtonDesktop = document.getElementById("user-dropdown-button");  | 
7 |  | -  const dropdownButtonMobile = document.getElementById("user-dropdown-button-mobile");  | 
8 |  | -  const dropdownMenu = document.getElementById("dropdown-user");  | 
9 |  | - | 
10 |  | -  if (dropdownMenu) {  | 
11 |  | -    const isHidden = dropdownMenu.classList.contains("hidden");  | 
12 |  | - | 
13 |  | -    if (isHidden) {  | 
14 |  | -      dropdownMenu.classList.remove("hidden");  | 
15 |  | - | 
16 |  | -      const menuLinks = dropdownMenu.querySelectorAll("a");  | 
17 |  | -      menuLinks.forEach((link) => {  | 
18 |  | -        // Close dropdown when clicking on an action before loading the new page  | 
19 |  | -        link.addEventListener("click", () => {  | 
20 |  | -          const menu = document.getElementById("dropdown-user");  | 
21 |  | -          if (menu) {  | 
22 |  | -            menu.classList.add("hidden");  | 
23 |  | -          }  | 
24 |  | -        });  | 
25 |  | -      });  | 
26 |  | - | 
27 |  | -      // Close dropdown when clicking outside  | 
28 |  | -      const closeOnClickOutside = (event) => {  | 
29 |  | -        const clickedOnDesktopButton = dropdownButtonDesktop && dropdownButtonDesktop.contains(event.target);  | 
30 |  | -        const clickedOnMobileButton = dropdownButtonMobile && dropdownButtonMobile.contains(event.target);  | 
31 |  | -        const clickedOnDropdown = dropdownMenu.contains(event.target);  | 
32 |  | - | 
33 |  | -        if (!clickedOnDropdown && !clickedOnDesktopButton && !clickedOnMobileButton) {  | 
34 |  | -          dropdownMenu.classList.add("hidden");  | 
35 |  | -          // Remove the event listener to prevent memory leaks  | 
36 |  | -          document.removeEventListener("click", closeOnClickOutside);  | 
37 |  | -        }  | 
38 |  | -      };  | 
39 |  | - | 
40 |  | -      // Add the event listener with a small delay to prevent immediate closure  | 
41 |  | -      setTimeout(() => {  | 
42 |  | -        document.addEventListener("click", closeOnClickOutside);  | 
43 |  | -      }, 10);  | 
44 |  | -    } else {  | 
45 |  | -      dropdownMenu.classList.add("hidden");  | 
 | 4 | +let documentHandlersBound = false;  | 
 | 5 | +let lifecycleListenersBound = false;  | 
 | 6 | + | 
 | 7 | +// Ensures global handlers close the dropdown on outside click or Escape.  | 
 | 8 | +const ensureDocumentHandlers = () => {  | 
 | 9 | +  if (documentHandlersBound) {  | 
 | 10 | +    return;  | 
 | 11 | +  }  | 
 | 12 | + | 
 | 13 | +  const handleDocumentClick = (event) => {  | 
 | 14 | +    const button = document.getElementById("user-dropdown-button");  | 
 | 15 | +    const dropdown = document.getElementById("user-dropdown");  | 
 | 16 | + | 
 | 17 | +    if (!button || !dropdown) {  | 
 | 18 | +      return;  | 
46 | 19 |     }  | 
 | 20 | + | 
 | 21 | +    const clickedButton = button.contains(event.target);  | 
 | 22 | +    const clickedDropdown = dropdown.contains(event.target);  | 
 | 23 | + | 
 | 24 | +    if (!clickedButton && !clickedDropdown) {  | 
 | 25 | +      // Hide if the click did not originate inside the dropdown or trigger.  | 
 | 26 | +      dropdown.classList.add("hidden");  | 
 | 27 | +    }  | 
 | 28 | +  };  | 
 | 29 | + | 
 | 30 | +  const handleKeydown = (event) => {  | 
 | 31 | +    if (event.key !== "Escape") {  | 
 | 32 | +      return;  | 
 | 33 | +    }  | 
 | 34 | + | 
 | 35 | +    const button = document.getElementById("user-dropdown-button");  | 
 | 36 | +    const dropdown = document.getElementById("user-dropdown");  | 
 | 37 | + | 
 | 38 | +    if (!button || !dropdown || dropdown.classList.contains("hidden")) {  | 
 | 39 | +      return;  | 
 | 40 | +    }  | 
 | 41 | + | 
 | 42 | +    dropdown.classList.add("hidden");  | 
 | 43 | +    button.focus();  | 
 | 44 | +  };  | 
 | 45 | + | 
 | 46 | +  document.addEventListener("click", handleDocumentClick);  | 
 | 47 | +  document.addEventListener("keydown", handleKeydown);  | 
 | 48 | + | 
 | 49 | +  documentHandlersBound = true;  | 
 | 50 | +};  | 
 | 51 | + | 
 | 52 | +// Subscribes to HTMX lifecycle hooks once for history and swap events.  | 
 | 53 | +const bindLifecycleListeners = () => {  | 
 | 54 | +  if (lifecycleListenersBound) {  | 
 | 55 | +    return;  | 
47 | 56 |   }  | 
 | 57 | + | 
 | 58 | +  document.addEventListener("htmx:historyRestore", initUserDropdown);  | 
 | 59 | +  document.addEventListener("htmx:afterSwap", initUserDropdown);  | 
 | 60 | +  window.addEventListener("pageshow", () => initUserDropdown());  | 
 | 61 | + | 
 | 62 | +  lifecycleListenersBound = true;  | 
48 | 63 | };  | 
 | 64 | + | 
 | 65 | +// Toggles dropdown visibility when the avatar button is clicked.  | 
 | 66 | +const toggleDropdownVisibility = (event) => {  | 
 | 67 | +  const dropdown = document.getElementById("user-dropdown");  | 
 | 68 | +  if (!dropdown) {  | 
 | 69 | +    return;  | 
 | 70 | +  }  | 
 | 71 | + | 
 | 72 | +  event.stopPropagation();  | 
 | 73 | +  dropdown.classList.toggle("hidden");  | 
 | 74 | +};  | 
 | 75 | + | 
 | 76 | +// Public initializer for the user dropdown interactions.  | 
 | 77 | +export const initUserDropdown = () => {  | 
 | 78 | +  ensureDocumentHandlers();  | 
 | 79 | +  bindLifecycleListeners();  | 
 | 80 | + | 
 | 81 | +  const button = document.getElementById("user-dropdown-button");  | 
 | 82 | +  const dropdown = document.getElementById("user-dropdown");  | 
 | 83 | + | 
 | 84 | +  if (!button || !dropdown || button.__ocgDropdownInitialized) {  | 
 | 85 | +    return;  | 
 | 86 | +  }  | 
 | 87 | + | 
 | 88 | +  button.addEventListener("click", toggleDropdownVisibility);  | 
 | 89 | +  button.__ocgDropdownInitialized = true;  | 
 | 90 | + | 
 | 91 | +  if (!dropdown.__ocgCloseOnLinkBound) {  | 
 | 92 | +    dropdown.addEventListener(  | 
 | 93 | +      "click",  | 
 | 94 | +      (event) => {  | 
 | 95 | +        const link = event.target.closest("a");  | 
 | 96 | +        if (!link) {  | 
 | 97 | +          return;  | 
 | 98 | +        }  | 
 | 99 | +        // Ensure selecting any link closes the dropdown immediately.  | 
 | 100 | +        dropdown.classList.add("hidden");  | 
 | 101 | +      },  | 
 | 102 | +      true,  | 
 | 103 | +    );  | 
 | 104 | +    dropdown.__ocgCloseOnLinkBound = true;  | 
 | 105 | +  }  | 
 | 106 | +};  | 
 | 107 | + | 
 | 108 | +initUserDropdown();  | 
0 commit comments