feat: Improve dropdown menu handling and visibility for OTS-specific menus

This commit is contained in:
Matt Batchelder
2026-02-07 00:48:41 -05:00
parent edd112fec3
commit 1c5c23f100
5 changed files with 197 additions and 132 deletions

View File

@@ -34,8 +34,8 @@
<p class="text-muted">{% trans "Manage time-based scheduling rules." %}</p>
</div>
{% embed 'custom/otssignange/views/partials/_dashboard-card.twig' with {'classes':'ots-displays-card'} %}
{% block body %}
<div class="widget dashboard-card ots-displays-card">
<div class="widget-body ots-displays-body">
<div class="XiboGrid" id="{{ random() }}">
<div class="XiboFilter card mb-3 bg-light dashboard-card ots-filter-card">
<div class="ots-filter-header">
@@ -46,43 +46,44 @@
</div>
<div class="ots-filter-content collapsed" id="ots-filter-content">
<div class="FilterDiv card-body" id="Filter">
<form class="form-inline">
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('name', title) }}
<form class="form-inline">
{% set title %}{% trans "Name" %}{% endset %}
{{ inline.inputNameGrid('name', title) }}
{% set title %}{% trans "Retired" %}{% endset %}
{% set option1 = "Yes"|trans %}
{% set option2 = "No"|trans %}
{% set values = [{id: 1, value: option1}, {id: 0, value: option2}] %}
{{ inline.dropdown("isRetired", "single", title, 0, values, "id", "value") }}
</form>
{% set title %}{% trans "Retired" %}{% endset %}
{% set option1 = "Yes"|trans %}
{% set option2 = "No"|trans %}
{% set values = [{id: 1, value: option1}, {id: 0, value: option2}] %}
{{ inline.dropdown("isRetired", "single", title, 0, values, "id", "value") }}
</form>
</div>
</div>
</div>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("daypart.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Daypart" %}" href="{{ url_for("daypart.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Daypart" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="dayparts" class="table table-striped" data-state-preference-name="daypartGrid">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Start Time" %}</th>
<th>{% trans "End Time" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
<div class="XiboData card pt-3 dashboard-card ots-table-card">
<div class="ots-table-toolbar">
{% if currentUser.featureEnabled("daypart.add") %}
<button class="btn btn-sm btn-success XiboFormButton" title="{% trans "Add a new Daypart" %}" href="{{ url_for("daypart.add.form") }}"><i class="fa fa-plus-circle" aria-hidden="true"></i> {% trans "Add Daypart" %}</button>
{% endif %}
<button class="btn btn-sm btn-primary" id="refreshGrid" title="{% trans "Refresh the Table" %}" href="#"><i class="fa fa-refresh" aria-hidden="true"></i></button>
</div>
<table id="dayparts" class="table table-striped" data-state-preference-name="daypartGrid">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Start Time" %}</th>
<th>{% trans "End Time" %}</th>
<th class="rowMenu"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</tbody>
</table>
</div>
</div>
{% endblock %}
{% endembed %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -315,79 +315,63 @@
group.classList.toggle('is-open', !isOpen);
target.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block';
}, true);
});
}
/**
* Initialize dropdown menus
*/
function initDropdowns() {
const dropdowns = document.querySelectorAll('.dropdown');
dropdowns.forEach(dropdown => {
const button = dropdown.querySelector('.dropdown-menu');
if (!button) return;
const menu = dropdown.querySelector('.dropdown-menu');
// Toggle menu on button click
dropdown.addEventListener('click', function(e) {
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]') || e.target.closest('#navbarUserMenu')) {
e.preventDefault();
const nowActive = !dropdown.classList.contains('active');
dropdown.classList.toggle('active');
// Only handle the user-menu dropdown.
// Let Bootstrap handle topbar nav dropdowns (Schedule, Design, etc.) natively
// so that links like Dayparting navigate normally.
const userDropdown = document.querySelector('#navbarUserMenu') && document.querySelector('#navbarUserMenu').closest('.dropdown');
if (!userDropdown) return;
// If the dropdown has a menu, float it out of any overflowed container
try {
const ddMenu = dropdown.querySelector('.dropdown-menu');
if (ddMenu) {
if (nowActive) {
floatMenu(ddMenu, dropdown);
} else {
unfloatMenu(ddMenu);
}
}
} catch (err) { /* ignore */ }
const userMenu = userDropdown.querySelector('.dropdown-menu');
if (!userMenu) return;
// If this dropdown contains the user menu, compute placement to avoid going off-screen
const menu = dropdown.querySelector('.dropdown-menu.ots-user-menu');
const trigger = dropdown.querySelector('#navbarUserMenu');
if (menu && trigger) {
// Reset any previous placement classes
menu.classList.remove('dropdown-menu-left');
menu.classList.remove('dropdown-menu-right');
userDropdown.addEventListener('click', function(e) {
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]') || e.target.closest('#navbarUserMenu')) {
e.preventDefault();
const nowActive = !userDropdown.classList.contains('active');
userDropdown.classList.toggle('active');
// Use getBoundingClientRect for accurate placement
const trigRect = trigger.getBoundingClientRect();
// Ensure menu is in DOM and has an offsetWidth
const menuWidth = menu.offsetWidth || 220; // fallback estimate
// Float / unfloat the user menu
try {
if (nowActive) {
floatMenu(userMenu, userDropdown);
} else {
unfloatMenu(userMenu);
}
} catch (err) { /* ignore */ }
const spaceRight = window.innerWidth - trigRect.right;
const spaceLeft = trigRect.left;
// Prefer opening to the right where possible, otherwise open to the left
if (spaceRight < menuWidth && spaceLeft > menuWidth) {
// not enough space on the right, open to left
menu.classList.add('dropdown-menu-left');
} else {
// default to right-aligned
menu.classList.add('dropdown-menu-right');
}
// Compute placement to avoid going off-screen
const trigger = userDropdown.querySelector('#navbarUserMenu');
if (trigger) {
userMenu.classList.remove('dropdown-menu-left', 'dropdown-menu-right');
const trigRect = trigger.getBoundingClientRect();
const menuWidth = userMenu.offsetWidth || 220;
const spaceRight = window.innerWidth - trigRect.right;
const spaceLeft = trigRect.left;
if (spaceRight < menuWidth && spaceLeft > menuWidth) {
userMenu.classList.add('dropdown-menu-left');
} else {
userMenu.classList.add('dropdown-menu-right');
}
}
});
// Close menu when clicking outside
document.addEventListener('click', function(e) {
if (!dropdown.contains(e.target)) {
const hasActive = dropdown.classList.contains('active');
dropdown.classList.remove('active');
if (hasActive) {
try { const ddMenu = dropdown.querySelector('.dropdown-menu'); if (ddMenu) unfloatMenu(ddMenu); } catch (err) {}
}
}
});
// Close user menu when clicking outside
document.addEventListener('click', function(e) {
if (!userDropdown.contains(e.target) && !userMenu.contains(e.target)) {
const hadActive = userDropdown.classList.contains('active');
userDropdown.classList.remove('active');
if (hadActive) {
try { unfloatMenu(userMenu); } catch (err) {}
}
});
}
});
}