Refactor page structure: Update page classes for consistency

- Changed class from "ots-displays-page" to "ots-static-page ots-displays-page" in multiple Twig view files to standardize page layout.
- Enhanced schedule-page.twig with improved calendar navigation and dropdown management.
- Added global dropdown dismissal functionality to improve user experience across modals and dropdowns.
This commit is contained in:
Matt Batchelder
2026-02-11 20:47:09 -05:00
parent 29b56bef4f
commit b766487411
34 changed files with 4506 additions and 141 deletions

View File

@@ -1131,6 +1131,9 @@
* (before Bootstrap sees it), prevent Bootstrap from handling it, and
* manage show/hide/position entirely ourselves.
*/
// Module-level reference to row dropdown's closeMenu, set by initRowDropdowns().
var _closeRowDropdown = null;
function initRowDropdowns() {
var TOGGLE_SEL = '.dropdown-menu-container [data-toggle="dropdown"], .btn-group [data-toggle="dropdown"]';
var activeMenu = null; // currently open menu element (in <body>)
@@ -1209,6 +1212,9 @@
activeTrigger = null;
}
// Expose closeMenu so closeAllDropdowns() can reach it
_closeRowDropdown = closeMenu;
// Intercept clicks in CAPTURE phase — runs BEFORE Bootstrap's handler.
document.addEventListener('click', function(e) {
var toggle = e.target.closest(TOGGLE_SEL);
@@ -1263,6 +1269,121 @@
});
}
/**
* Close every open dropdown / popover on the page.
* Covers: Bootstrap 4 native dropdowns, OTS custom dropdowns,
* the user-menu, notification drawer, and DataTable row menus.
*/
function closeAllDropdowns() {
try {
// 0. Close row dropdown managed by initRowDropdowns()
if (typeof _closeRowDropdown === 'function') _closeRowDropdown();
// Also force-remove any stray ots-row-dropdown elements left on <body>
document.querySelectorAll('.ots-row-dropdown').forEach(function(m) {
m.classList.remove('show', 'ots-row-dropdown');
m.style.cssText = '';
});
// 1. Bootstrap 4 native dropdowns (.show on the wrapper or the menu)
document.querySelectorAll('.dropdown.show, .btn-group.show').forEach(function(el) {
el.classList.remove('show');
var m = el.querySelector('.dropdown-menu.show');
if (m) m.classList.remove('show');
});
document.querySelectorAll('.dropdown-menu.show').forEach(function(m) {
m.classList.remove('show');
});
// 2. OTS custom dropdowns that use .active
document.querySelectorAll('.dropdown.active, .ots-topbar-action .dropdown.active, .ots-page-actions .dropdown.active').forEach(function(el) {
el.classList.remove('active');
});
// 3. User menu (body-level floating menu)
var userMenu = document.querySelector('.ots-user-menu-body.ots-user-menu-open');
if (userMenu) {
userMenu.classList.remove('ots-user-menu-open');
var userDropdown = document.querySelector('#navbarUserMenu');
if (userDropdown) {
var dd = userDropdown.closest('.dropdown');
if (dd) dd.classList.remove('active', 'show');
}
}
// 4. DataTable button collections
document.querySelectorAll('.dt-buttons.active').forEach(function(w) {
w.classList.remove('active');
});
document.querySelectorAll('.dt-button-collection.show').forEach(function(c) {
c.classList.remove('show');
c.style.display = 'none';
});
// 5. jQuery-level Bootstrap cleanup (if available)
var jq = window.jQuery || window.$;
if (jq) {
jq('.dropdown-toggle[aria-expanded="true"]').attr('aria-expanded', 'false');
jq('.dropdown-menu.show').removeClass('show');
jq('.dropdown.show, .btn-group.show').removeClass('show');
}
} catch (err) {
// never let this break the page
}
}
/**
* Wire up global listeners that trigger closeAllDropdowns().
* Called once from init().
*/
function initGlobalDropdownDismiss() {
// ── Close when a Bootstrap modal / dialog opens ─────────────────
document.addEventListener('show.bs.modal', closeAllDropdowns, true);
try {
var jq = window.jQuery || window.$;
if (jq) {
jq(document).on('show.bs.modal', closeAllDropdowns);
// Xibo opens modals when .XiboFormButton / .XiboAjaxSubmit are clicked
jq(document).on('click', '.XiboFormButton, .XiboAjaxSubmit, .XiboFormRender', function() {
closeAllDropdowns();
});
}
} catch (e) {}
// ── Close when any <a> inside a dropdown is clicked (page nav) ──
document.addEventListener('click', function(e) {
var link = e.target.closest('.dropdown-menu a, .ots-topbar a.nav-link, .ots-topbar a.dropdown-item, .XiboFormButton');
if (link && !e.defaultPrevented) {
closeAllDropdowns();
}
});
// ── Close on History navigation (Xibo uses pushState for AJAX pages) ──
window.addEventListener('popstate', closeAllDropdowns);
// Intercept pushState / replaceState so we catch Xibo's AJAX navigation
try {
var origPush = history.pushState;
var origReplace = history.replaceState;
history.pushState = function() {
origPush.apply(this, arguments);
closeAllDropdowns();
};
history.replaceState = function() {
origReplace.apply(this, arguments);
closeAllDropdowns();
};
} catch (e) {}
// ── Close when main content area changes (AJAX page swap) ───────
try {
var content = document.getElementById('content') || document.querySelector('.ots-content');
if (content) {
var contentObs = new MutationObserver(debounce(closeAllDropdowns, 80));
contentObs.observe(content, { childList: true });
}
} catch (e) {}
}
/**
* Initialize all features when DOM is ready
*/
@@ -1273,6 +1394,7 @@
initThemeToggle();
initDropdowns();
initRowDropdowns();
initGlobalDropdownDismiss();
initSearch();
initPageInteractions();
initDataTables();
@@ -1301,6 +1423,57 @@
}
})();
/**
* OTS: Enhance all Xibo form modals to match the upload modal design.
* Runs on every shown.bs.modal event and also exposed as window.otsEnhanceModal()
* for direct invocation from form callbacks like mediaEditFormOpen.
*/
(function() {
'use strict';
var OTS_CLOSE_SVG = '<button type="button" class="ots-upload-close" data-dismiss="modal" aria-label="Close">' +
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 5L5 15M5 5l10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>' +
'</button>';
function enhanceModal(modal) {
var $m = window.jQuery ? window.jQuery(modal) : null;
if (!$m || !$m.length) return;
// Don't re-enhance
if ($m.data('ots-enhanced')) return;
$m.data('ots-enhanced', true);
// Skip the custom upload modal (it has its own styling)
if ($m.hasClass('ots-upload-modal') || $m.attr('id') === 'ots-upload-modal') return;
// Add the OTS edit modal class
$m.addClass('ots-edit-media-modal');
// Replace the default close button with SVG version
var $closeBtn = $m.find('.modal-header .close, .modal-header button[data-dismiss="modal"]:not(.ots-upload-close)');
if ($closeBtn.length) {
$closeBtn.first().replaceWith(OTS_CLOSE_SVG);
}
}
// Expose globally so page callbacks can invoke it directly
window.otsEnhanceModal = enhanceModal;
// Hook into every modal show event
if (window.jQuery) {
window.jQuery(document).on('shown.bs.modal', '.modal', function() {
enhanceModal(this);
});
} else {
document.addEventListener('shown.bs.modal', function(e) {
var modal = e.target;
if (modal && modal.classList && modal.classList.contains('modal')) {
enhanceModal(modal);
}
}, true);
}
})();
// Replace broken QR images in user profile modals with a friendly placeholder
function initUserProfileQrFix() {
function replaceIfEmptyDataUri(el) {