Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 103 additions & 43 deletions ocg-server/static/js/common/header.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,108 @@
/**
* Toggles the user dropdown menu visibility and manages event listeners.
* Handles click-outside-to-close functionality.
* Initializes header dropdown behavior with HTMX awareness.
*/
export const onClickDropdown = () => {
const dropdownButtonDesktop = document.getElementById("user-dropdown-button");
const dropdownButtonMobile = document.getElementById("user-dropdown-button-mobile");
const dropdownMenu = document.getElementById("dropdown-user");

if (dropdownMenu) {
const isHidden = dropdownMenu.classList.contains("hidden");

if (isHidden) {
dropdownMenu.classList.remove("hidden");

const menuLinks = dropdownMenu.querySelectorAll("a");
menuLinks.forEach((link) => {
// Close dropdown when clicking on an action before loading the new page
link.addEventListener("click", () => {
const menu = document.getElementById("dropdown-user");
if (menu) {
menu.classList.add("hidden");
}
});
});

// Close dropdown when clicking outside
const closeOnClickOutside = (event) => {
const clickedOnDesktopButton = dropdownButtonDesktop && dropdownButtonDesktop.contains(event.target);
const clickedOnMobileButton = dropdownButtonMobile && dropdownButtonMobile.contains(event.target);
const clickedOnDropdown = dropdownMenu.contains(event.target);

if (!clickedOnDropdown && !clickedOnDesktopButton && !clickedOnMobileButton) {
dropdownMenu.classList.add("hidden");
// Remove the event listener to prevent memory leaks
document.removeEventListener("click", closeOnClickOutside);
}
};

// Add the event listener with a small delay to prevent immediate closure
setTimeout(() => {
document.addEventListener("click", closeOnClickOutside);
}, 10);
} else {
dropdownMenu.classList.add("hidden");
let documentHandlersBound = false;
let lifecycleListenersBound = false;

// Ensures global handlers close the dropdown on outside click or Escape.
const ensureDocumentHandlers = () => {
if (documentHandlersBound) {
return;
}

const handleDocumentClick = (event) => {
const button = document.getElementById("user-dropdown-button");
const dropdown = document.getElementById("user-dropdown");

if (!button || !dropdown) {
return;
}

const clickedButton = button.contains(event.target);
const clickedDropdown = dropdown.contains(event.target);

if (!clickedButton && !clickedDropdown) {
// Hide if the click did not originate inside the dropdown or trigger.
dropdown.classList.add("hidden");
}
};

const handleKeydown = (event) => {
if (event.key !== "Escape") {
return;
}

const button = document.getElementById("user-dropdown-button");
const dropdown = document.getElementById("user-dropdown");

if (!button || !dropdown || dropdown.classList.contains("hidden")) {
return;
}

dropdown.classList.add("hidden");
button.focus();
};

document.addEventListener("click", handleDocumentClick);
document.addEventListener("keydown", handleKeydown);

documentHandlersBound = true;
};

// Subscribes to HTMX lifecycle hooks once for history and swap events.
const bindLifecycleListeners = () => {
if (lifecycleListenersBound) {
return;
}

document.addEventListener("htmx:historyRestore", initUserDropdown);
document.addEventListener("htmx:afterSwap", initUserDropdown);
window.addEventListener("pageshow", () => initUserDropdown());

lifecycleListenersBound = true;
};

// Toggles dropdown visibility when the avatar button is clicked.
const toggleDropdownVisibility = (event) => {
const dropdown = document.getElementById("user-dropdown");
if (!dropdown) {
return;
}

event.stopPropagation();
dropdown.classList.toggle("hidden");
};

// Public initializer for the user dropdown interactions.
export const initUserDropdown = () => {
ensureDocumentHandlers();
bindLifecycleListeners();

const button = document.getElementById("user-dropdown-button");
const dropdown = document.getElementById("user-dropdown");

if (!button || !dropdown || button.__ocgDropdownInitialized) {
return;
}

button.addEventListener("click", toggleDropdownVisibility);
button.__ocgDropdownInitialized = true;

if (!dropdown.__ocgCloseOnLinkBound) {
dropdown.addEventListener(
"click",
(event) => {
const link = event.target.closest("a");
if (!link) {
return;
}
// Ensure selecting any link closes the dropdown immediately.
dropdown.classList.add("hidden");
},
true,
);
dropdown.__ocgCloseOnLinkBound = true;
}
};

initUserDropdown();
30 changes: 7 additions & 23 deletions ocg-server/templates/common/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@
<div class="flex items-center">
<div class="svg-icon size-4 icon-buildings bg-stone-600"></div>
<div class="ms-2 text-xs/6">Community Dashboard</div>
<div class="ms-auto hx-spinner">{% call common::spinner(size = "size-4") -%}</div>
</div>
</a>
</li>
Expand All @@ -244,6 +245,7 @@
<div class="flex items-center">
<div class="svg-icon size-4 icon-groups bg-stone-600"></div>
<div class="ms-2 text-xs/6">Group Dashboard</div>
<div class="ms-auto hx-spinner">{% call common::spinner(size = "size-4") -%}</div>
</div>
</a>
</li>
Expand All @@ -258,6 +260,7 @@
<div class="flex items-center">
<div class="svg-icon size-4 icon-user-small bg-stone-600"></div>
<div class="ms-2 text-xs/6">User Dashboard</div>
<div class="ms-auto hx-spinner">{% call common::spinner(size = "size-4") -%}</div>
</div>
</a>
</li>
Expand Down Expand Up @@ -348,30 +351,11 @@

{# Dropdown script -#}
<script type="module">
const userDropdownButton = document.getElementById('user-dropdown-button');
const userDropdown = document.getElementById('user-dropdown');
import {
initUserDropdown
} from '/static/js/common/header.js';

if (userDropdownButton && userDropdown) {
userDropdownButton.addEventListener('click', (e) => {
e.stopPropagation();
userDropdown.classList.toggle('hidden');
});

// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!userDropdownButton.contains(e.target) && !userDropdown.contains(e.target)) {
userDropdown.classList.add('hidden');
}
});

// Close dropdown on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !userDropdown.classList.contains('hidden')) {
userDropdown.classList.add('hidden');
userDropdownButton.focus();
}
});
}
initUserDropdown();
</script>
</div>
{% endmacro user_menu -%}
Expand Down