Refactor toolbar buttons across various pages to unify styling
- Updated button classes for consistency in the playersoftware-page, playlist-page, resolution-page, schedule-page, settings-page, syncgroup-page, tag-page, task-page, template-page, transition-page, user-page, and usergroup-page. - Removed unnecessary text from button titles and ensured all buttons have the 'ots-toolbar-btn' class for uniformity. - Cleaned up the code by removing commented-out sections and ensuring proper indentation.
This commit is contained in:
@@ -34,12 +34,10 @@
|
||||
* This function is kept as a no-op for backward compatibility.
|
||||
*/
|
||||
function updateSidebarWidth() {
|
||||
// No-op: CSS handles layout via body.ots-sidebar-collapsed class
|
||||
if (window.__otsDebug) {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const collapsed = sidebar ? sidebar.classList.contains('collapsed') : false;
|
||||
console.log('[OTS] updateSidebarWidth (no-op, CSS-driven)', { collapsed });
|
||||
}
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const w = sidebar.offsetWidth;
|
||||
document.documentElement.style.setProperty('--ots-sidebar-actual-width', w + 'px');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,38 +45,41 @@
|
||||
* so nav items always begin below the header (logo + buttons).
|
||||
*/
|
||||
function updateSidebarNavOffset() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const header = sidebar.querySelector('.sidebar-header');
|
||||
const nav = sidebar.querySelector('.sidebar-nav, .ots-sidebar-nav');
|
||||
if (!nav) return;
|
||||
const sidebarRect = sidebar.getBoundingClientRect();
|
||||
const headerRect = header ? header.getBoundingClientRect() : null;
|
||||
let offset = 0;
|
||||
if (headerRect) {
|
||||
offset = Math.max(0, Math.ceil(headerRect.bottom - sidebarRect.top));
|
||||
} else if (header) {
|
||||
offset = header.offsetHeight || 0;
|
||||
}
|
||||
const gap = 8;
|
||||
const paddingTop = offset > 0 ? offset + gap : '';
|
||||
if (paddingTop) {
|
||||
try {
|
||||
nav.style.setProperty('padding-top', `${paddingTop}px`, 'important');
|
||||
} catch (err) {
|
||||
nav.style.paddingTop = `${paddingTop}px`;
|
||||
}
|
||||
if (window.__otsDebug) console.log('[OTS] updateSidebarNavOffset applied', { paddingTop });
|
||||
} else {
|
||||
try {
|
||||
nav.style.removeProperty('padding-top');
|
||||
} catch (err) {
|
||||
nav.style.paddingTop = '';
|
||||
}
|
||||
if (window.__otsDebug) console.log('[OTS] updateSidebarNavOffset cleared');
|
||||
/* No-op: sidebar uses flex-direction:column so the header and
|
||||
nav content are separate flex children that never overlap.
|
||||
Previously this set padding-top:~72px which created a huge gap. */
|
||||
var nav = document.querySelector('.ots-sidebar .sidebar-nav, .ots-sidebar .ots-sidebar-nav');
|
||||
if (nav) {
|
||||
try { nav.style.removeProperty('padding-top'); } catch(e) { nav.style.paddingTop = ''; }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the playlist/layout editor modal is open and toggle
|
||||
* body.ots-playlist-editor-active accordingly. Because the editor is
|
||||
* loaded via AJAX into #editor-container, a one-shot check at page-load
|
||||
* is not enough – we use a MutationObserver that watches for DOM changes.
|
||||
*/
|
||||
function updatePlaylistEditorBackground() {
|
||||
var isActive = !!document.querySelector('.editor-modal, #playlist-editor, #layout-editor');
|
||||
document.body.classList.toggle('ots-playlist-editor-active', isActive);
|
||||
}
|
||||
|
||||
/* Start a MutationObserver that fires updatePlaylistEditorBackground
|
||||
whenever children are added to or removed from the page. */
|
||||
(function initEditorObserver() {
|
||||
// Run once immediately
|
||||
updatePlaylistEditorBackground();
|
||||
|
||||
var target = document.body;
|
||||
if (!target) return;
|
||||
|
||||
var editorObs = new MutationObserver(function() {
|
||||
updatePlaylistEditorBackground();
|
||||
});
|
||||
editorObs.observe(target, { childList: true, subtree: true });
|
||||
})();
|
||||
|
||||
/**
|
||||
* DISABLED: Cleanup function to remove inline styles that were forcing incorrect margins
|
||||
* The sidebar layout is now controlled entirely by CSS variables and margin-left.
|
||||
@@ -225,8 +226,12 @@
|
||||
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
|
||||
syncSubmenuDisplayForState(nowCollapsed);
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarStateClass();
|
||||
// Update measured width immediately and again after CSS transition
|
||||
updateSidebarWidth();
|
||||
setTimeout(updateSidebarWidth, 250);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,8 +242,12 @@
|
||||
body.classList.remove('ots-sidebar-collapsed');
|
||||
document.documentElement.classList.remove('ots-sidebar-collapsed');
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
|
||||
syncSubmenuDisplayForState(false);
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarStateClass();
|
||||
// Update measured width immediately and again after CSS transition
|
||||
updateSidebarWidth();
|
||||
setTimeout(updateSidebarWidth, 250);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,6 +268,69 @@
|
||||
updateSidebarStateClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build flyout headers for each sidebar-submenu.
|
||||
* Pulls the icon class(es) and label from the parent group toggle
|
||||
* and injects a styled header <li> at the top of the submenu.
|
||||
* Idempotent — skips submenus that already have a header.
|
||||
*/
|
||||
function buildFlyoutHeaders() {
|
||||
var groups = document.querySelectorAll('.sidebar-group');
|
||||
groups.forEach(function(group) {
|
||||
var submenu = group.querySelector('.sidebar-submenu');
|
||||
if (!submenu) return;
|
||||
if (submenu.querySelector('.flyout-header')) return;
|
||||
|
||||
var toggle = group.querySelector('.sidebar-group-toggle');
|
||||
if (!toggle) return;
|
||||
|
||||
var iconEl = toggle.querySelector('.ots-nav-icon');
|
||||
var textEl = toggle.querySelector('.ots-nav-text');
|
||||
if (!textEl) return;
|
||||
|
||||
var label = textEl.textContent.trim();
|
||||
|
||||
var header = document.createElement('li');
|
||||
header.className = 'flyout-header';
|
||||
header.setAttribute('aria-hidden', 'true');
|
||||
|
||||
if (iconEl) {
|
||||
var icon = document.createElement('span');
|
||||
icon.className = iconEl.className;
|
||||
icon.classList.add('flyout-header-icon');
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
header.appendChild(icon);
|
||||
}
|
||||
|
||||
var text = document.createElement('span');
|
||||
text.className = 'flyout-header-text';
|
||||
text.textContent = label;
|
||||
header.appendChild(text);
|
||||
|
||||
submenu.insertBefore(header, submenu.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When toggling between collapsed/expanded, sync all submenu inline
|
||||
* display styles so that:
|
||||
* - Collapsed: no inline display → CSS :hover handles flyouts
|
||||
* - Expanded: inline display block/none based on is-open state
|
||||
*/
|
||||
function syncSubmenuDisplayForState(isCollapsed) {
|
||||
var groups = document.querySelectorAll('.sidebar-group');
|
||||
groups.forEach(function(group) {
|
||||
var submenu = group.querySelector('.sidebar-submenu');
|
||||
if (!submenu) return;
|
||||
if (isCollapsed) {
|
||||
submenu.style.removeProperty('display');
|
||||
} else {
|
||||
var isOpen = group.classList.contains('is-open');
|
||||
submenu.style.display = isOpen ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize sidebar section collapse/expand functionality
|
||||
*/
|
||||
@@ -268,10 +340,15 @@
|
||||
groupToggles.forEach(toggle => {
|
||||
const group = toggle.closest('.sidebar-group');
|
||||
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
|
||||
const caret = toggle.querySelector('.sidebar-group-caret');
|
||||
if (submenu) {
|
||||
const isOpen = group.classList.contains('is-open');
|
||||
submenu.style.display = isOpen ? 'block' : 'none';
|
||||
const sidebarEl = document.querySelector('.ots-sidebar');
|
||||
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
|
||||
if (!isCollapsed) {
|
||||
submenu.style.display = isOpen ? 'block' : 'none';
|
||||
} else {
|
||||
submenu.style.removeProperty('display');
|
||||
}
|
||||
toggle.setAttribute('aria-expanded', isOpen.toString());
|
||||
}
|
||||
|
||||
@@ -282,39 +359,16 @@
|
||||
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
|
||||
if (!submenu) return;
|
||||
|
||||
const sidebarEl = document.querySelector('.ots-sidebar');
|
||||
const isCollapsed = sidebarEl && sidebarEl.classList.contains('collapsed');
|
||||
if (isCollapsed) return;
|
||||
|
||||
const isOpen = group.classList.contains('is-open');
|
||||
group.classList.toggle('is-open', !isOpen);
|
||||
toggle.setAttribute('aria-expanded', (!isOpen).toString());
|
||||
submenu.style.display = isOpen ? 'none' : 'block';
|
||||
requestAnimationFrame(updateSidebarWidth);
|
||||
});
|
||||
|
||||
if (caret) {
|
||||
caret.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggle.click();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Capture-phase handler to override any conflicting listeners
|
||||
document.addEventListener('click', function(e) {
|
||||
const caret = e.target.closest('.sidebar-group-caret');
|
||||
const toggle = e.target.closest('.sidebar-group-toggle');
|
||||
const target = toggle || (caret ? caret.closest('.sidebar-group-toggle') : null);
|
||||
if (!target) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const group = target.closest('.sidebar-group');
|
||||
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
|
||||
if (!submenu) return;
|
||||
|
||||
const isOpen = group.classList.contains('is-open');
|
||||
group.classList.toggle('is-open', !isOpen);
|
||||
target.setAttribute('aria-expanded', (!isOpen).toString());
|
||||
submenu.style.display = isOpen ? 'none' : 'block';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1001,92 +1055,145 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Move DataTable row dropdown menus to <body> so they escape
|
||||
* any overflow:hidden / overflow:auto ancestor containers.
|
||||
* DataTable row action dropdowns — fully managed by OTS theme.
|
||||
*
|
||||
* Xibo renders the row action button as:
|
||||
* <div class="btn-group pull-right dropdown-menu-container">
|
||||
* <button class="btn btn-white dropdown-toggle" data-toggle="dropdown">
|
||||
* <div class="dropdown-menu dropdown-menu-right">...items...</div>
|
||||
* </div>
|
||||
* Bootstrap 4 + Popper.js positions menus with transform: translate3d(),
|
||||
* but the theme CSS sets transform: none !important which breaks that.
|
||||
* Detaching the menu to <body> also triggers Bootstrap's hide event.
|
||||
*
|
||||
* Bootstrap fires shown/hide.bs.dropdown on the toggle's parent element
|
||||
* (the .btn-group). We listen on document with a selector that matches
|
||||
* the actual Xibo markup.
|
||||
* Solution: intercept the click on the toggle button in the capture phase
|
||||
* (before Bootstrap sees it), prevent Bootstrap from handling it, and
|
||||
* manage show/hide/position entirely ourselves.
|
||||
*/
|
||||
function initRowDropdowns() {
|
||||
var DROPDOWN_PARENT_SEL = '.dropdown-menu-container, .btn-group';
|
||||
var SCOPE_SEL = '.XiboData ' + DROPDOWN_PARENT_SEL
|
||||
+ ', .XiboGrid ' + DROPDOWN_PARENT_SEL
|
||||
+ ', #datatable-container ' + DROPDOWN_PARENT_SEL
|
||||
+ ', .dataTables_wrapper ' + DROPDOWN_PARENT_SEL;
|
||||
var TOGGLE_SEL = '.dropdown-menu-container [data-toggle="dropdown"], .btn-group [data-toggle="dropdown"]';
|
||||
var activeMenu = null; // currently open menu element (in <body>)
|
||||
var activeParent = null; // original parent (.btn-group / .dropdown-menu-container)
|
||||
var activeTrigger = null;
|
||||
|
||||
$(document).on('shown.bs.dropdown', SCOPE_SEL, function(e) {
|
||||
var $container = $(this);
|
||||
var $trigger = $container.find('[data-toggle="dropdown"]');
|
||||
var $menu = $container.find('.dropdown-menu');
|
||||
if (!$menu.length || !$trigger.length) return;
|
||||
function openMenu(trigger) {
|
||||
closeMenu(); // close any previously open menu first
|
||||
|
||||
// Mark the menu so we can style it and find it later
|
||||
$menu.addClass('ots-row-dropdown');
|
||||
var $trigger = $(trigger);
|
||||
var $parent = $trigger.closest('.dropdown-menu-container, .btn-group');
|
||||
var $menu = $parent.find('.dropdown-menu').first();
|
||||
if (!$menu.length) return;
|
||||
|
||||
// Store original parent so we can put it back on close
|
||||
$menu.data('ots-original-parent', $container);
|
||||
activeTrigger = trigger;
|
||||
activeParent = $parent[0];
|
||||
|
||||
// Get trigger position in viewport
|
||||
var btnRect = $trigger[0].getBoundingClientRect();
|
||||
// Snapshot button position while menu is still in the normal DOM
|
||||
var btnRect = trigger.getBoundingClientRect();
|
||||
|
||||
// Move to body
|
||||
// Detach menu and append to body so it escapes overflow:hidden
|
||||
$menu.detach().appendTo('body');
|
||||
activeMenu = $menu[0];
|
||||
|
||||
// Position below the trigger button, aligned to the right edge
|
||||
// Make visible-but-hidden so we can measure it
|
||||
activeMenu.style.cssText = 'display:block !important; visibility:hidden !important; position:fixed !important; transform:none !important;';
|
||||
var menuW = $menu.outerWidth() || 180;
|
||||
var menuH = $menu.outerHeight() || 200;
|
||||
|
||||
// Compute position: below button, right-aligned to button's right edge
|
||||
var top = btnRect.bottom + 2;
|
||||
var left = btnRect.right - $menu.outerWidth();
|
||||
var left = btnRect.right - menuW;
|
||||
|
||||
// Keep within viewport bounds
|
||||
// Viewport bounds check
|
||||
if (left < 8) left = 8;
|
||||
if (top + $menu.outerHeight() > window.innerHeight - 8) {
|
||||
// Open upward if no room below
|
||||
top = btnRect.top - $menu.outerHeight() - 2;
|
||||
if (left + menuW > window.innerWidth - 8) left = window.innerWidth - menuW - 8;
|
||||
if (top + menuH > window.innerHeight - 8) {
|
||||
top = btnRect.top - menuH - 2; // flip above the button
|
||||
}
|
||||
if (top < 8) top = 8;
|
||||
|
||||
$menu.css({
|
||||
position: 'fixed',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
display: 'block'
|
||||
});
|
||||
});
|
||||
// Apply final position — every property with !important
|
||||
activeMenu.style.cssText = [
|
||||
'position:fixed !important',
|
||||
'top:' + top + 'px !important',
|
||||
'left:' + left + 'px !important',
|
||||
'right:auto !important',
|
||||
'bottom:auto !important',
|
||||
'display:block !important',
|
||||
'visibility:visible !important',
|
||||
'transform:none !important',
|
||||
'will-change:auto !important',
|
||||
'margin:0 !important',
|
||||
'z-index:2147483647 !important'
|
||||
].join(';') + ';';
|
||||
|
||||
// When the dropdown closes, move the menu back to its original parent
|
||||
$(document).on('hide.bs.dropdown', SCOPE_SEL, function(e) {
|
||||
var $container = $(this);
|
||||
var $menu = $('body > .dropdown-menu.ots-row-dropdown').filter(function() {
|
||||
var orig = $(this).data('ots-original-parent');
|
||||
return orig && orig[0] === $container[0];
|
||||
});
|
||||
if ($menu.length) {
|
||||
$menu.removeClass('ots-row-dropdown').css({ position: '', top: '', left: '', display: '' });
|
||||
$menu.detach().appendTo($container);
|
||||
$menu.addClass('ots-row-dropdown show');
|
||||
$parent.addClass('show');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
if (!activeMenu) return;
|
||||
var $menu = $(activeMenu);
|
||||
var $parent = $(activeParent);
|
||||
|
||||
// Clear all inline styles
|
||||
activeMenu.style.cssText = '';
|
||||
$menu.removeClass('ots-row-dropdown show');
|
||||
|
||||
// Move menu back to its original parent
|
||||
$menu.detach().appendTo($parent);
|
||||
$parent.removeClass('show open');
|
||||
|
||||
activeMenu = null;
|
||||
activeParent = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
// Intercept clicks in CAPTURE phase — runs BEFORE Bootstrap's handler.
|
||||
document.addEventListener('click', function(e) {
|
||||
var toggle = e.target.closest(TOGGLE_SEL);
|
||||
if (!toggle) {
|
||||
// Click was not on a toggle — close any open menu
|
||||
// (unless click is inside the open menu itself)
|
||||
if (activeMenu && !e.target.closest('.ots-row-dropdown')) {
|
||||
closeMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle toggles inside DataTable areas
|
||||
var inTable = toggle.closest('.XiboData') || toggle.closest('.XiboGrid')
|
||||
|| toggle.closest('#datatable-container') || toggle.closest('.dataTables_wrapper');
|
||||
if (!inTable) return; // not a row dropdown — let Bootstrap handle it
|
||||
|
||||
// Prevent Bootstrap from handling this dropdown
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Toggle behaviour: if same trigger, close; otherwise open
|
||||
if (activeTrigger === toggle && activeMenu) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu(toggle);
|
||||
}
|
||||
}, true); // ← true = capture phase
|
||||
|
||||
// Close on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && activeMenu) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Also close any orphaned body-appended dropdown on outside click
|
||||
$(document).on('click', function(e) {
|
||||
var $openMenus = $('body > .dropdown-menu.ots-row-dropdown');
|
||||
if (!$openMenus.length) return;
|
||||
// If the click is inside the menu itself, let it through
|
||||
if ($(e.target).closest('.ots-row-dropdown').length) return;
|
||||
$openMenus.each(function() {
|
||||
var $menu = $(this);
|
||||
var $parent = $menu.data('ots-original-parent');
|
||||
if ($parent && $parent.length) {
|
||||
$menu.removeClass('ots-row-dropdown').css({ position: '', top: '', left: '', display: '' });
|
||||
$menu.detach().appendTo($parent);
|
||||
$parent.removeClass('open show');
|
||||
}
|
||||
});
|
||||
// Close on any scroll (window or scrollable ancestor)
|
||||
window.addEventListener('scroll', function() {
|
||||
if (activeMenu) closeMenu();
|
||||
}, true);
|
||||
|
||||
// Block Bootstrap's show/hide events for DataTable row dropdowns
|
||||
// so it doesn't interfere with our manual management.
|
||||
$(document).on('show.bs.dropdown hide.bs.dropdown', function(e) {
|
||||
var toggle = e.relatedTarget;
|
||||
if (!toggle) return;
|
||||
var inTable = toggle.closest('.XiboData') || toggle.closest('.XiboGrid')
|
||||
|| toggle.closest('#datatable-container') || toggle.closest('.dataTables_wrapper');
|
||||
if (inTable) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1096,6 +1203,7 @@
|
||||
function init() {
|
||||
initSidebarToggle();
|
||||
initSidebarSectionToggles();
|
||||
buildFlyoutHeaders();
|
||||
initThemeToggle();
|
||||
initDropdowns();
|
||||
initRowDropdowns();
|
||||
@@ -1107,11 +1215,13 @@
|
||||
initChartSafeguard();
|
||||
updateSidebarWidth();
|
||||
updateSidebarNavOffset();
|
||||
updatePlaylistEditorBackground();
|
||||
// updateSidebarGap() disabled - use CSS variables instead
|
||||
initUserProfileQrFix();
|
||||
var debouncedUpdate = debounce(function() {
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarWidth();
|
||||
updatePlaylistEditorBackground();
|
||||
// updateSidebarGap() disabled - use CSS variables instead
|
||||
}, 120);
|
||||
window.addEventListener('resize', debouncedUpdate);
|
||||
|
||||
Reference in New Issue
Block a user