From 1c5c23f100b3b6606fc9699ce4355c3b964b5c45 Mon Sep 17 00:00:00 2001 From: Matt Batchelder Date: Sat, 7 Feb 2026 00:48:41 -0500 Subject: [PATCH] feat: Improve dropdown menu handling and visibility for OTS-specific menus --- _xibo-cms-clone | 1 + custom/otssignange/css/override.css | 141 +++++++++++++++----- custom/otssignange/js/theme.js | 16 ++- custom/otssignange/views/daypart-page.twig | 67 +++++----- custom/otssignange/views/theme-scripts.twig | 104 ++++++--------- 5 files changed, 197 insertions(+), 132 deletions(-) create mode 160000 _xibo-cms-clone diff --git a/_xibo-cms-clone b/_xibo-cms-clone new file mode 160000 index 0000000..56c98da --- /dev/null +++ b/_xibo-cms-clone @@ -0,0 +1 @@ +Subproject commit 56c98da0b586da18d5f39a9e78177f9164bc0de6 diff --git a/custom/otssignange/css/override.css b/custom/otssignange/css/override.css index 8d8a504..f6527b3 100644 --- a/custom/otssignange/css/override.css +++ b/custom/otssignange/css/override.css @@ -80,14 +80,25 @@ body { transform: none !important; } +/* Ensure menus that are intentionally floated into `body` are visible + (this targets only menus moved by our JS: `.ots-floating-menu` or + `.dropdown-menu` marked with `data-ots-floating="1`). This avoids + forcing visibility for all dropdowns. */ +.ots-floating-menu, +.dropdown-menu[data-ots-floating="1"] { + display: block !important; + visibility: visible !important; + opacity: 1 !important; +} + /* Elevated z-index for menus so they render above other panels */ .ots-floating-menu, .dropdown-menu { z-index: 99999 !important; } -/* Force ALL dropdown/context menu classes to render on top of everything. - This is maximally aggressive to defeat any stacking context or overflow clipping. */ -.dropdown-menu, +/* Limit aggressive floating/menu overrides to explicit floating menus only. + Avoid forcing `.dropdown-menu` to be `display:block`/`visibility:visible` so + native dropdown behaviour (open/close) is preserved. */ .ots-notif-menu, .ots-user-menu, .context-menu, @@ -101,24 +112,29 @@ body { transform: none !important; will-change: auto !important; pointer-events: auto !important; - visibility: visible !important; - display: block !important; - opacity: 1 !important; } -/* Ensure dropdown list items and children aren't clipped */ -.dropdown-menu li, -.dropdown-menu > li > a, -.dropdown-menu > li > span, -.dropdown-menu ul, -.dropdown-menu div { +/* Hide the rowMenu column header completely */ +th.rowMenu, +td.rowMenu { + display: none !important; + visibility: hidden !important; + width: 0 !important; + padding: 0 !important; + margin: 0 !important; + border: 0 !important; +} + +/* Ensure floating menu children aren't clipped (scoped to floated menus only) */ +.ots-floating-menu li, +.ots-floating-menu > li > a, +.ots-floating-menu > li > span, +.ots-floating-menu ul, +.ots-floating-menu div { overflow: visible !important; - position: relative !important; - z-index: 2147483647 !important; } -/* Remove any transform or overflow from menu ancestors that could create stacking context */ -.dropdown-menu *, +/* Remove transforms inside floating menus that could create stacking context */ .ots-floating-menu * { transform: none !important; } @@ -1994,6 +2010,12 @@ body.ots-sidebar-open .ots-topbar { display: inline-block; } +.dataTables_buttons, +.dataTables_buttons * { + background: transparent !important; + color: var(--color-text-primary) !important; +} + .dt-button-collection { position: absolute; top: 100%; @@ -2081,6 +2103,19 @@ body.ots-sidebar-open .ots-topbar { color: var(--color-primary) !important; } +/* DataTables ColVis button styling */ +.ColVis_MasterButton, +.dt-button { + background-color: var(--color-surface-elevated) !important; + color: var(--color-text-primary) !important; + border: 1px solid var(--color-border) !important; +} + +.ColVis_MasterButton:hover, +.dt-button:hover { + background-color: var(--color-surface) !important; +} + .dropdown.active .dropdown-menu, .dropdown:focus-within .dropdown-menu { display: block; @@ -3147,6 +3182,35 @@ body.ots-sidebar-open .ots-topbar { display: block; } +/* Ensure the folder-tree "All Folders" checkbox follows light/dark variables + (the checkbox lives in `.ots-folder-tree` / `#grid-folder-filter`). */ +.ots-folder-tree input[type="checkbox"], +.ots-folder-tree .form-check-input, +#grid-folder-filter input[type="checkbox"], +#grid-folder-filter .form-check-input { + -webkit-appearance: none !important; + appearance: none !important; + width: 18px !important; + height: 18px !important; + display: inline-block !important; + vertical-align: middle !important; + border-radius: 4px !important; + background-color: var(--color-surface) !important; + background-image: none !important; + border: 1px solid var(--color-border) !important; + box-shadow: none !important; +} + +.ots-folder-tree input[type="checkbox"]:checked, +.ots-folder-tree .form-check-input:checked, +#grid-folder-filter input[type="checkbox"]:checked, +#grid-folder-filter .form-check-input:checked { + background-color: var(--color-primary) !important; + border-color: var(--color-primary) !important; + background-image: none !important; + box-shadow: none !important; +} + .ots-grid-with-folders { display: grid; grid-template-columns: 260px 1fr; @@ -3255,6 +3319,12 @@ body.ots-sidebar-open .ots-topbar { border-bottom: 1px solid rgba(148, 163, 184, 0.2); } +/* Make table fill full width */ +.ots-table-card .table { + width: 100% !important; + table-layout: auto !important; +} + .ots-table-card .table thead th, .ots-table-card .table tbody td, .ots-table-card .table tbody th, @@ -4262,7 +4332,9 @@ textarea { .panel, .panel-body, .table-wrapper, -.dataTables_wrapper .dataTables_scrollBody { +.dataTables_wrapper .dataTables_scrollBody, +.dataTables_wrapper .dataTables_scrollHead, +.dataTables_wrapper .dataTables_scrollHead table { opacity: 1 !important; background: transparent !important; } @@ -5118,33 +5190,32 @@ body.ots-light-mode .dataTables_wrapper .dataTables_paginate .paginate_button.cu color: white !important; border-color: var(--color-primary) !important; } -/* Aggressive unconditional overrides to ensure table text is readable - These apply regardless of light/dark mode */ -.table td, -.table th, -.dataTable td, -.dataTable th, -.dataTables_wrapper td, -.dataTables_wrapper th, -.modern-table td, -.modern-table th { +/* Light-mode unconditional overrides to ensure table text is readable */ +body.ots-light-mode .table td, +body.ots-light-mode .table th, +body.ots-light-mode .dataTable td, +body.ots-light-mode .dataTable th, +body.ots-light-mode .dataTables_wrapper td, +body.ots-light-mode .dataTables_wrapper th, +body.ots-light-mode .modern-table td, +body.ots-light-mode .modern-table th { color: #0f172a !important; } -.dataTables_wrapper .dataTables_info, -.dataTables_wrapper .dataTables_filter, -.dataTables_wrapper .dataTables_length { +body.ots-light-mode .dataTables_wrapper .dataTables_info, +body.ots-light-mode .dataTables_wrapper .dataTables_filter, +body.ots-light-mode .dataTables_wrapper .dataTables_length { color: #0f172a !important; } -.table thead, -.dataTable thead, -.modern-table thead { +body.ots-light-mode .table thead, +body.ots-light-mode .dataTable thead, +body.ots-light-mode .modern-table thead { background-color: #f1f5f9 !important; } -.dataTables_wrapper .dataTables_filter input, -.dataTables_wrapper .dataTables_length select { +body.ots-light-mode .dataTables_wrapper .dataTables_filter input, +body.ots-light-mode .dataTables_wrapper .dataTables_length select { color: #0f172a !important; background-color: #ffffff !important; border-color: #e2e8f0 !important; diff --git a/custom/otssignange/js/theme.js b/custom/otssignange/js/theme.js index b814092..ccf0543 100644 --- a/custom/otssignange/js/theme.js +++ b/custom/otssignange/js/theme.js @@ -306,10 +306,13 @@ * Initialize dropdown menus */ function initDropdowns() { - const dropdowns = document.querySelectorAll('.dropdown'); + // Only handle OTS-specific dropdowns (user menu, notifications). + // Let Bootstrap handle topbar nav dropdowns (Schedule, Design, etc.) natively + // so that links like Dayparting can navigate normally. + const otsDropdowns = document.querySelectorAll('.ots-topbar-action .dropdown, .ots-page-actions .dropdown'); - dropdowns.forEach(dropdown => { - const toggle = dropdown.querySelector('.dropdown-toggle, [data-toggle="dropdown"], .dt-button'); + otsDropdowns.forEach(dropdown => { + const toggle = dropdown.querySelector('.dropdown-toggle, [data-toggle="dropdown"]'); const menu = dropdown.querySelector('.dropdown-menu'); if (!toggle || !menu) return; @@ -318,7 +321,12 @@ toggle.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); - dropdown.classList.toggle('active'); + const isNowActive = dropdown.classList.toggle('active'); + + // Close other OTS dropdowns + otsDropdowns.forEach(other => { + if (other !== dropdown) other.classList.remove('active'); + }); }); // Close menu when clicking outside diff --git a/custom/otssignange/views/daypart-page.twig b/custom/otssignange/views/daypart-page.twig index 111614e..c794dcf 100644 --- a/custom/otssignange/views/daypart-page.twig +++ b/custom/otssignange/views/daypart-page.twig @@ -34,8 +34,8 @@

{% trans "Manage time-based scheduling rules." %}

- {% embed 'custom/otssignange/views/partials/_dashboard-card.twig' with {'classes':'ots-displays-card'} %} - {% block body %} +
+
@@ -46,43 +46,44 @@
-
-
- {% if currentUser.featureEnabled("daypart.add") %} - - {% endif %} - -
- - - - - - - - - - - +
+
+ {% if currentUser.featureEnabled("daypart.add") %} + + {% endif %} + +
+
{% trans "Name" %}{% trans "Description" %}{% trans "Start Time" %}{% trans "End Time" %}
+ + + + + + + + + + - -
{% trans "Name" %}{% trans "Description" %}{% trans "Start Time" %}{% trans "End Time" %}
+ + +
- {% endblock %} - {% endembed %} +
+
{% endblock %} diff --git a/custom/otssignange/views/theme-scripts.twig b/custom/otssignange/views/theme-scripts.twig index 1c4436d..28a5f24 100644 --- a/custom/otssignange/views/theme-scripts.twig +++ b/custom/otssignange/views/theme-scripts.twig @@ -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) {} } - }); + } }); }