Add new pages for managing tags, tasks, transitions, users, user groups, and their respective JavaScript functionalities
- Implemented tag management page with filtering, data table, and AJAX functionality. - Created task management page with task listing, filtering, and AJAX data loading. - Developed transition management page with a data table for transitions. - Added user management page with comprehensive user details, filtering options, and AJAX support. - Introduced user group management page with filtering and data table for user groups. - Enhanced JavaScript for data tables, including state saving, filtering, and AJAX data fetching for all new pages.
This commit is contained in:
@@ -30,44 +30,18 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* Measure sidebar width and set CSS variable for layout
|
||||
* Sidebar width is now handled purely by CSS classes (.ots-sidebar-collapsed).
|
||||
* This function is kept as a no-op for backward compatibility.
|
||||
*/
|
||||
function updateSidebarWidth() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const collapsed = sidebar.classList.contains('collapsed');
|
||||
// If called with a forced mode, use the stored defaults
|
||||
const forceMode = updateSidebarWidth._forceMode || null;
|
||||
const base = (forceMode === 'full') ? (window.__otsFullSidebarWidth || 256)
|
||||
: (forceMode === 'collapsed') ? (window.__otsCollapsedSidebarWidth || 70)
|
||||
: (collapsed ? 70 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 240);
|
||||
const padding = 5;
|
||||
const value = Math.max(70, Math.round(base + padding));
|
||||
// Apply CSS variable used by layout and also set an inline width fallback
|
||||
document.documentElement.style.setProperty('--ots-sidebar-width', `${value}px`);
|
||||
try {
|
||||
// Inline width helps force an immediate reflow when CSS rules/important flags interfere
|
||||
// Use setProperty with 'important' so stylesheet !important rules can't override it.
|
||||
sidebar.style.setProperty('width', `${value}px`, 'important');
|
||||
// Force reflow to encourage the browser to apply the new sizing immediately
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
sidebar.offsetHeight;
|
||||
} catch (err) {
|
||||
try { sidebar.style.width = `${value}px`; } catch (e) { /* ignore */ }
|
||||
}
|
||||
// Debug logging to help identify timing/specifity issues in the wild
|
||||
// No-op: CSS handles layout via body.ots-sidebar-collapsed class
|
||||
if (window.__otsDebug) {
|
||||
console.log('[OTS] updateSidebarWidth', { collapsed, base, value, cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width') });
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const collapsed = sidebar ? sidebar.classList.contains('collapsed') : false;
|
||||
console.log('[OTS] updateSidebarWidth (no-op, CSS-driven)', { collapsed });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to request a forced width update
|
||||
function forceSidebarWidthMode(mode) {
|
||||
updateSidebarWidth._forceMode = mode; // 'full' | 'collapsed' | null
|
||||
updateSidebarWidth();
|
||||
updateSidebarWidth._forceMode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the sidebar header bottom and set the top padding of the nav list
|
||||
* so nav items always begin below the header (logo + buttons).
|
||||
@@ -106,86 +80,34 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the sidebar and set an explicit left margin on the page wrapper
|
||||
* so the gap between the sidebar and page content is exactly 5px.
|
||||
* 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.
|
||||
*/
|
||||
function updateSidebarGap() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
// target likely content containers in this app
|
||||
// This function is intentionally left minimal.
|
||||
// Spacing is now handled by CSS: .ots-main { margin-left: var(--ots-sidebar-width) }
|
||||
// Removing any inline margin-left or padding-left that may have been set previously
|
||||
const targets = [
|
||||
document.getElementById('page-wrapper'),
|
||||
document.querySelector('.ots-main'),
|
||||
document.getElementById('content-wrapper'),
|
||||
document.querySelector('#content')
|
||||
].filter(Boolean);
|
||||
if (!sidebar || !targets.length) return;
|
||||
|
||||
const gap = (typeof window.__otsDesiredSidebarGap !== 'undefined') ? Number(window.__otsDesiredSidebarGap) : 0; // desired gap in px (default 0)
|
||||
const rect = sidebar.getBoundingClientRect();
|
||||
|
||||
// desired inner left padding (allows trimming space inside the content area)
|
||||
const desiredInnerPadding = (typeof window.__otsDesiredPagePaddingLeft !== 'undefined') ? Number(window.__otsDesiredPagePaddingLeft) : 8;
|
||||
|
||||
targets.forEach(pageWrapper => {
|
||||
const pageRect = pageWrapper.getBoundingClientRect();
|
||||
const computed = window.getComputedStyle(pageWrapper);
|
||||
const currentMargin = parseFloat(computed.marginLeft) || 0;
|
||||
const currentGap = Math.round(pageRect.left - rect.right);
|
||||
// Calculate how much to adjust margin-left so gap becomes `gap`.
|
||||
const delta = currentGap - gap;
|
||||
const newMargin = Math.max(0, Math.round(currentMargin - delta));
|
||||
try {
|
||||
pageWrapper.style.setProperty('margin-left', `${newMargin}px`, 'important');
|
||||
pageWrapper.style.setProperty('padding-left', `${desiredInnerPadding}px`, 'important');
|
||||
pageWrapper.style.removeProperty('margin-left');
|
||||
pageWrapper.style.removeProperty('padding-left');
|
||||
} catch (err) {
|
||||
pageWrapper.style.marginLeft = `${newMargin}px`;
|
||||
pageWrapper.style.paddingLeft = `${desiredInnerPadding}px`;
|
||||
pageWrapper.style.marginLeft = '';
|
||||
pageWrapper.style.paddingLeft = '';
|
||||
}
|
||||
// Also adjust common child wrapper padding if present
|
||||
// Also remove from common child wrappers
|
||||
try {
|
||||
const inner = pageWrapper.querySelector('.page-content') || pageWrapper.querySelector('.ots-content') || pageWrapper.querySelector('.container');
|
||||
if (inner) inner.style.setProperty('padding-left', `${desiredInnerPadding}px`, 'important');
|
||||
} catch (err) {}
|
||||
if (window.__otsDebug) console.log('[OTS] updateSidebarGap', {
|
||||
target: pageWrapper.tagName + (pageWrapper.id ? '#'+pageWrapper.id : ''),
|
||||
sidebarWidth: rect.width,
|
||||
sidebarRight: rect.right,
|
||||
pageLeft: pageRect.left,
|
||||
currentGap,
|
||||
newMargin
|
||||
});
|
||||
|
||||
// Detect narrow intervening elements (visual separator) and neutralize their visuals
|
||||
try {
|
||||
const sampleXs = [Math.round(rect.right + 2), Math.round((rect.right + pageRect.left) / 2), Math.round(pageRect.left - 2)];
|
||||
const ys = [Math.floor(window.innerHeight / 2), Math.floor(window.innerHeight / 4), Math.floor(window.innerHeight * 0.75)];
|
||||
const seen = new Set();
|
||||
sampleXs.forEach(x => {
|
||||
ys.forEach(y => {
|
||||
try {
|
||||
const els = document.elementsFromPoint(x, y) || [];
|
||||
els.forEach(el => {
|
||||
if (!el || el === document.documentElement || el === document.body) return;
|
||||
if (el === sidebar || el === pageWrapper) return;
|
||||
const b = el.getBoundingClientRect();
|
||||
// narrow vertical candidates between sidebar and content
|
||||
if (b.left >= rect.right - 4 && b.right <= pageRect.left + 4 && b.width <= 80 && b.height >= 40) {
|
||||
const id = el.tagName + (el.id ? '#'+el.id : '') + (el.className ? '.'+el.className.split(' ').join('.') : '');
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
try {
|
||||
el.style.setProperty('background', 'transparent', 'important');
|
||||
el.style.setProperty('background-image', 'none', 'important');
|
||||
el.style.setProperty('box-shadow', 'none', 'important');
|
||||
el.style.setProperty('border', 'none', 'important');
|
||||
el.style.setProperty('pointer-events', 'none', 'important');
|
||||
if (window.__otsDebug) console.log('[OTS] neutralized intervening element', { id, rect: b });
|
||||
} catch (err) {}
|
||||
}
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
});
|
||||
if (inner) {
|
||||
inner.style.removeProperty('padding-left');
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
@@ -225,11 +147,65 @@
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
// Mobile-aware toggle: add backdrop, aria-expanded, and focus management
|
||||
if (toggleBtn) {
|
||||
let lastFocus = null;
|
||||
function ensureBackdrop() {
|
||||
let bd = document.querySelector('.ots-backdrop');
|
||||
if (!bd) {
|
||||
bd = document.createElement('div');
|
||||
bd.className = 'ots-backdrop';
|
||||
bd.addEventListener('click', function() {
|
||||
sidebar.classList.remove('active');
|
||||
bd.classList.remove('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
if (lastFocus) lastFocus.focus();
|
||||
});
|
||||
document.body.appendChild(bd);
|
||||
}
|
||||
return bd;
|
||||
}
|
||||
|
||||
toggleBtn.setAttribute('role', 'button');
|
||||
toggleBtn.setAttribute('aria-controls', 'ots-sidebar');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const isNowActive = !sidebar.classList.contains('active');
|
||||
sidebar.classList.toggle('active');
|
||||
// On small screens show backdrop and manage focus
|
||||
if (window.innerWidth <= 768) {
|
||||
const bd = ensureBackdrop();
|
||||
if (isNowActive) {
|
||||
bd.classList.add('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'true');
|
||||
lastFocus = document.activeElement;
|
||||
const firstFocusable = sidebar.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (firstFocusable) firstFocusable.focus(); else { sidebar.setAttribute('tabindex', '-1'); sidebar.focus(); }
|
||||
document.addEventListener('keydown', escHandler);
|
||||
} else {
|
||||
bd.classList.remove('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
if (lastFocus) lastFocus.focus();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
}
|
||||
updateSidebarStateClass();
|
||||
});
|
||||
|
||||
function escHandler(e) {
|
||||
if (e.key === 'Escape' || e.key === 'Esc') {
|
||||
const bd = document.querySelector('.ots-backdrop');
|
||||
if (sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
if (bd) bd.classList.remove('show');
|
||||
toggleBtn.setAttribute('aria-expanded', 'false');
|
||||
if (lastFocus) lastFocus.focus();
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (collapseBtn) {
|
||||
@@ -237,8 +213,9 @@
|
||||
if (isCollapsed) {
|
||||
sidebar.classList.add('collapsed');
|
||||
body.classList.add('ots-sidebar-collapsed');
|
||||
document.documentElement.classList.add('ots-sidebar-collapsed');
|
||||
updateSidebarStateClass();
|
||||
updateSidebarGap();
|
||||
// updateSidebarGap() disabled - use CSS variables instead
|
||||
}
|
||||
|
||||
collapseBtn.addEventListener('click', function(e) {
|
||||
@@ -246,26 +223,10 @@
|
||||
const nowCollapsed = !sidebar.classList.contains('collapsed');
|
||||
sidebar.classList.toggle('collapsed');
|
||||
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
document.documentElement.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
|
||||
// Force collapsed width immediately
|
||||
forceSidebarWidthMode('collapsed');
|
||||
// Recalculate nav offset so items remain below header after collapse
|
||||
updateSidebarNavOffset();
|
||||
// Ensure page content gap is updated for collapsed width
|
||||
updateSidebarGap();
|
||||
// Re-run shortly after to catch any late layout changes
|
||||
setTimeout(updateSidebarGap, 80);
|
||||
updateSidebarStateClass();
|
||||
// Debug state after toggle
|
||||
try {
|
||||
console.log('[OTS] collapseBtn clicked', {
|
||||
nowCollapsed,
|
||||
classes: sidebar.className,
|
||||
inlineStyle: sidebar.getAttribute('style'),
|
||||
computedWidth: getComputedStyle(sidebar).width,
|
||||
cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width')
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,23 +235,10 @@
|
||||
e.preventDefault();
|
||||
sidebar.classList.remove('collapsed');
|
||||
body.classList.remove('ots-sidebar-collapsed');
|
||||
document.documentElement.classList.remove('ots-sidebar-collapsed');
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
|
||||
// Force full width when expanding
|
||||
forceSidebarWidthMode('full');
|
||||
// Recalculate nav offset after expanding
|
||||
updateSidebarNavOffset();
|
||||
// Ensure page content gap is updated for expanded width
|
||||
updateSidebarGap();
|
||||
setTimeout(updateSidebarGap, 80);
|
||||
updateSidebarStateClass();
|
||||
try {
|
||||
console.log('[OTS] expandBtn clicked', {
|
||||
classes: sidebar.className,
|
||||
inlineStyle: sidebar.getAttribute('style'),
|
||||
computedWidth: getComputedStyle(sidebar).width,
|
||||
cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width')
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -583,8 +531,7 @@
|
||||
} else {
|
||||
sidebar.classList.add('mobile');
|
||||
}
|
||||
updateSidebarWidth();
|
||||
updateSidebarGap();
|
||||
// updateSidebarGap() disabled - use CSS variables instead
|
||||
});
|
||||
}
|
||||
|
||||
@@ -651,7 +598,6 @@
|
||||
input.placeholder = 'Search…';
|
||||
input.className = 'table-search-input';
|
||||
input.setAttribute('aria-label', 'Table search');
|
||||
input.style.minWidth = '180px';
|
||||
|
||||
controls.appendChild(input);
|
||||
|
||||
@@ -751,6 +697,96 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move DataTable row dropdown menus to <body> so they escape
|
||||
* any overflow:hidden / overflow:auto ancestor containers.
|
||||
*
|
||||
* 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 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.
|
||||
*/
|
||||
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;
|
||||
|
||||
$(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;
|
||||
|
||||
// Mark the menu so we can style it and find it later
|
||||
$menu.addClass('ots-row-dropdown');
|
||||
|
||||
// Store original parent so we can put it back on close
|
||||
$menu.data('ots-original-parent', $container);
|
||||
|
||||
// Get trigger position in viewport
|
||||
var btnRect = $trigger[0].getBoundingClientRect();
|
||||
|
||||
// Move to body
|
||||
$menu.detach().appendTo('body');
|
||||
|
||||
// Position below the trigger button, aligned to the right edge
|
||||
var top = btnRect.bottom + 2;
|
||||
var left = btnRect.right - $menu.outerWidth();
|
||||
|
||||
// Keep within viewport bounds
|
||||
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 (top < 8) top = 8;
|
||||
|
||||
$menu.css({
|
||||
position: 'fixed',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
display: 'block'
|
||||
});
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features when DOM is ready
|
||||
*/
|
||||
@@ -759,6 +795,7 @@
|
||||
initSidebarSectionToggles();
|
||||
initThemeToggle();
|
||||
initDropdowns();
|
||||
initRowDropdowns();
|
||||
initSearch();
|
||||
initPageInteractions();
|
||||
initDataTables();
|
||||
@@ -767,11 +804,11 @@
|
||||
initChartSafeguard();
|
||||
updateSidebarWidth();
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarGap();
|
||||
// updateSidebarGap() disabled - use CSS variables instead
|
||||
var debouncedUpdate = debounce(function() {
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarWidth();
|
||||
updateSidebarGap();
|
||||
// updateSidebarGap() disabled - use CSS variables instead
|
||||
}, 120);
|
||||
window.addEventListener('resize', debouncedUpdate);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user