Skip to content

Commit 0b57900

Browse files
authored
Improve header links and functionality (#192)
Signed-off-by: Cintia Sánchez García <[email protected]>
1 parent ecd2652 commit 0b57900

File tree

2 files changed

+110
-66
lines changed

2 files changed

+110
-66
lines changed
Lines changed: 103 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,108 @@
11
/**
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.
43
*/
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;
4619
}
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;
4756
}
57+
58+
document.addEventListener("htmx:historyRestore", initUserDropdown);
59+
document.addEventListener("htmx:afterSwap", initUserDropdown);
60+
window.addEventListener("pageshow", () => initUserDropdown());
61+
62+
lifecycleListenersBound = true;
4863
};
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();

ocg-server/templates/common/header.html

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@
229229
<div class="flex items-center">
230230
<div class="svg-icon size-4 icon-buildings bg-stone-600"></div>
231231
<div class="ms-2 text-xs/6">Community Dashboard</div>
232+
<div class="ms-auto hx-spinner">{% call common::spinner(size = "size-4") -%}</div>
232233
</div>
233234
</a>
234235
</li>
@@ -244,6 +245,7 @@
244245
<div class="flex items-center">
245246
<div class="svg-icon size-4 icon-groups bg-stone-600"></div>
246247
<div class="ms-2 text-xs/6">Group Dashboard</div>
248+
<div class="ms-auto hx-spinner">{% call common::spinner(size = "size-4") -%}</div>
247249
</div>
248250
</a>
249251
</li>
@@ -258,6 +260,7 @@
258260
<div class="flex items-center">
259261
<div class="svg-icon size-4 icon-user-small bg-stone-600"></div>
260262
<div class="ms-2 text-xs/6">User Dashboard</div>
263+
<div class="ms-auto hx-spinner">{% call common::spinner(size = "size-4") -%}</div>
261264
</div>
262265
</a>
263266
</li>
@@ -348,30 +351,11 @@
348351

349352
{# Dropdown script -#}
350353
<script type="module">
351-
const userDropdownButton = document.getElementById('user-dropdown-button');
352-
const userDropdown = document.getElementById('user-dropdown');
354+
import {
355+
initUserDropdown
356+
} from '/static/js/common/header.js';
353357

354-
if (userDropdownButton && userDropdown) {
355-
userDropdownButton.addEventListener('click', (e) => {
356-
e.stopPropagation();
357-
userDropdown.classList.toggle('hidden');
358-
});
359-
360-
// Close dropdown when clicking outside
361-
document.addEventListener('click', (e) => {
362-
if (!userDropdownButton.contains(e.target) && !userDropdown.contains(e.target)) {
363-
userDropdown.classList.add('hidden');
364-
}
365-
});
366-
367-
// Close dropdown on Escape key
368-
document.addEventListener('keydown', (e) => {
369-
if (e.key === 'Escape' && !userDropdown.classList.contains('hidden')) {
370-
userDropdown.classList.add('hidden');
371-
userDropdownButton.focus();
372-
}
373-
});
374-
}
358+
initUserDropdown();
375359
</script>
376360
</div>
377361
{% endmacro user_menu -%}

0 commit comments

Comments
 (0)