pre-img swap

This commit is contained in:
Matt Batchelder
2026-03-23 21:09:27 -04:00
parent 87474b05a9
commit bbe8c1860c
395 changed files with 29643 additions and 712 deletions

View File

@@ -78,6 +78,9 @@
updatePlaylistEditorBackground();
});
editorObs.observe(target, { childList: true, subtree: true });
// Store reference for cleanup
window._otsEditorObs = editorObs;
})();
/**
@@ -210,7 +213,8 @@
}
if (collapseBtn) {
const isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true';
let isCollapsed = false;
try { isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true'; } catch(e) {}
if (isCollapsed) {
sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed');
@@ -225,7 +229,7 @@
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');
try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false'); } catch(e) {}
syncSubmenuDisplayForState(nowCollapsed);
updateSidebarNavOffset();
updateSidebarStateClass();
@@ -241,7 +245,7 @@
sidebar.classList.remove('collapsed');
body.classList.remove('ots-sidebar-collapsed');
document.documentElement.classList.remove('ots-sidebar-collapsed');
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
try { localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false'); } catch(e) {}
syncSubmenuDisplayForState(false);
updateSidebarNavOffset();
updateSidebarStateClass();
@@ -372,6 +376,43 @@
});
}
/**
* Arrow key navigation within sidebar submenus.
* Up/Down moves between links, Escape collapses the group.
*/
function initSidebarKeyboardNav() {
var nav = document.querySelector('.ots-sidebar nav, .ots-sidebar [role="navigation"]');
if (!nav) return;
nav.addEventListener('keydown', function(e) {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Escape') return;
var submenu = e.target.closest('.sidebar-submenu');
if (!submenu) return;
var links = submenu.querySelectorAll('a:not([disabled])');
if (!links.length) return;
if (e.key === 'Escape') {
e.preventDefault();
var group = submenu.closest('.sidebar-group');
var toggle = group ? group.querySelector('.sidebar-group-toggle') : null;
if (toggle) toggle.click();
if (toggle) toggle.focus();
return;
}
e.preventDefault();
var idx = Array.prototype.indexOf.call(links, e.target);
if (e.key === 'ArrowDown') {
idx = idx < links.length - 1 ? idx + 1 : 0;
} else {
idx = idx > 0 ? idx - 1 : links.length - 1;
}
links[idx].focus();
});
}
/**
* Initialize dropdown menus
*/
@@ -744,56 +785,30 @@
});
});
// Continuously guard: check that menus stay in body and have correct styles
let guardInterval = setInterval(function() {
try {
selectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
// If menu got moved back, move it to body again
if (el.parentElement !== document.body && el.parentElement !== null) {
document.body.appendChild(el);
}
// Reapply critical styles in case they got overridden
applyMenuStyles(el);
});
});
} catch (err) {}
}, 200);
// Keep guard alive for the page lifetime, but stop if no menus found after 30s
let noMenuCount = 0;
const checkGuard = setInterval(function() {
const hasMenus = selectors.some(sel => document.querySelector(sel));
if (!hasMenus) {
noMenuCount++;
if (noMenuCount > 150) {
clearInterval(guardInterval);
clearInterval(checkGuard);
}
} else {
noMenuCount = 0;
}
}, 200);
// Observe for dynamically added menus
// Use a MutationObserver instead of polling to detect new menus
try {
const mo = new MutationObserver(function(muts) {
muts.forEach(m => {
(m.addedNodes || []).forEach(node => {
var forceMenuObs = new MutationObserver(function(muts) {
muts.forEach(function(m) {
(m.addedNodes || []).forEach(function(node) {
try {
if (!node || node.nodeType !== 1) return;
selectors.forEach(sel => {
selectors.forEach(function(sel) {
if (node.matches && node.matches(sel)) {
moveToBody(node);
applyMenuStyles(node);
}
const found = node.querySelectorAll && node.querySelectorAll(sel);
found && found.forEach(moveToBody);
var found = node.querySelectorAll && node.querySelectorAll(sel);
if (found) found.forEach(function(el) {
moveToBody(el);
applyMenuStyles(el);
});
});
} catch (err) {}
});
});
});
mo.observe(document.body, { childList: true, subtree: true });
forceMenuObs.observe(document.body, { childList: true, subtree: true });
window._otsForceMenuObs = forceMenuObs;
} catch (err) {}
}
@@ -834,7 +849,7 @@
if (filterCollapseBtn && filterContent) {
const storageKey = `ots-filter-collapsed:${window.location.pathname}`;
let isCollapsed = false;
let isCollapsed = filterContent.classList.contains('collapsed');
filterCollapseBtn.addEventListener('click', function() {
isCollapsed = !isCollapsed;
@@ -842,23 +857,28 @@
// Rotate icon
const icon = filterCollapseBtn.querySelector('i');
icon.classList.toggle('fa-chevron-up');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-up', !isCollapsed);
icon.classList.toggle('fa-chevron-down', isCollapsed);
// Save preference to localStorage
localStorage.setItem(storageKey, isCollapsed);
try { localStorage.setItem(storageKey, isCollapsed); } catch(e) {}
});
// Restore saved preference
const savedState = localStorage.getItem(storageKey);
// Restore saved preference (overrides HTML default)
let savedState = null;
try { savedState = localStorage.getItem(storageKey); } catch(e) {}
if (savedState === 'true') {
isCollapsed = true;
filterContent.classList.add('collapsed');
const icon = filterCollapseBtn.querySelector('i');
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
} else {
} else if (savedState === 'false') {
isCollapsed = false;
filterContent.classList.remove('collapsed');
const icon = filterCollapseBtn.querySelector('i');
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
}
}
@@ -871,27 +891,17 @@
let debounceTimeout;
const syncFolderLayout = () => {
// Check actual visibility using computed styles
const computedStyle = window.getComputedStyle(folderTree);
const isHidden = computedStyle.display === 'none' ||
computedStyle.visibility === 'hidden' ||
folderTree.offsetHeight === 0;
console.log('Folder collapse sync:', {
isHidden,
display: computedStyle.display,
visibility: computedStyle.visibility,
offsetHeight: folderTree.offsetHeight
requestAnimationFrame(() => {
// Batch all reads first
const computedStyle = window.getComputedStyle(folderTree);
const display = computedStyle.display;
const visibility = computedStyle.visibility;
const height = folderTree.offsetHeight;
const isHidden = display === 'none' || visibility === 'hidden' || height === 0;
// Then write
folderContainer.classList.toggle('ots-folder-collapsed', !!isHidden);
});
folderContainer.classList.toggle('ots-folder-collapsed', !!isHidden);
// Log the result
console.log('Container classes:', folderContainer.className);
console.log('Grid template columns:', window.getComputedStyle(folderContainer).gridTemplateColumns);
// Force reflow
folderContainer.offsetHeight;
};
const debouncedSync = () => {
@@ -901,7 +911,6 @@
// Watch for style/class changes on folderTree (let Xibo's code run first)
const treeObserver = new MutationObserver(() => {
console.log('Folder tree mutation detected, debouncing sync...');
debouncedSync();
});
treeObserver.observe(folderTree, {
@@ -1055,6 +1064,55 @@
const $ = window.jQuery;
if (!$.fn || !$.fn.dataTable) return;
// ── Override global DataTables template for modern layout ──
// Only if Xibo hasn't already set a custom template
if (typeof window.dataTablesTemplate === 'string') {
// Keep Xibo's template but ensure it includes our improvements
} else if (typeof window.dataTablesTemplate === 'undefined') {
window.dataTablesTemplate = '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>rt<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>';
}
// ── Enhance Xibo DataTables on draw ──
$(document).on('draw.dt', function(e, settings) {
try {
var api = new $.fn.dataTable.Api(settings);
var wrapper = $(api.table().container());
var tbody = $(api.table().body());
// Inject custom empty state when table has no visible rows
if (api.rows({ search: 'applied' }).count() === 0) {
var existing = wrapper.find('.ots-table-empty-state');
if (!existing.length) {
var emptyHtml = '<div class="ots-table-empty-state">' +
'<div class="ots-empty-icon"><i class="fa fa-inbox"></i></div>' +
'<div class="ots-empty-text">No results found</div>' +
'<div class="ots-empty-hint">Try adjusting your filters or search terms</div>' +
'</div>';
// Insert after the table, before pagination
var tableEl = $(api.table().node());
tableEl.after(emptyHtml);
}
} else {
wrapper.find('.ots-table-empty-state').remove();
}
// Apply status badge styling to known status cells
tbody.find('td .label, td .badge').each(function() {
var el = $(this);
if (el.hasClass('ots-badge')) return;
var text = (el.text() || '').toLowerCase().trim();
var cls = 'ots-badge ots-badge--neutral';
if (/online|active|enabled|yes|licensed|authorised/.test(text)) cls = 'ots-badge ots-badge--success';
else if (/offline|inactive|disabled|no|expired|revoked/.test(text)) cls = 'ots-badge ots-badge--danger';
else if (/pending|waiting|unknown|checking/.test(text)) cls = 'ots-badge ots-badge--warning';
else if (/edit|draft|building/.test(text)) cls = 'ots-badge ots-badge--info';
el.addClass(cls);
});
} catch (err) {
// DataTable enhancement failure is non-critical
}
});
// Skip Xibo-managed grids to avoid double initialization
if (document.querySelector('.XiboGrid')) return;
@@ -1103,7 +1161,7 @@
e.preventDefault();
const isLight = body.classList.toggle('ots-light-mode');
root.classList.toggle('ots-light-mode', isLight);
localStorage.setItem('ots-theme-mode', isLight ? 'light' : 'dark');
try { localStorage.setItem('ots-theme-mode', isLight ? 'light' : 'dark'); } catch(e) {}
updateThemeLabel();
});
@@ -1244,10 +1302,26 @@
}
}, true); // ← true = capture phase
// Close on Escape key
// Close on Escape key + arrow navigation within open menus
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && activeMenu) {
closeMenu();
return;
}
if (!activeMenu) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
var items = activeMenu.querySelectorAll('.dropdown-item:not(.disabled):not([disabled])');
if (!items.length) return;
var idx = Array.prototype.indexOf.call(items, document.activeElement);
if (e.key === 'ArrowDown') {
idx = idx < items.length - 1 ? idx + 1 : 0;
} else {
idx = idx > 0 ? idx - 1 : items.length - 1;
}
items[idx].focus();
}
});
@@ -1390,6 +1464,7 @@
function init() {
initSidebarToggle();
initSidebarSectionToggles();
initSidebarKeyboardNav();
buildFlyoutHeaders();
initThemeToggle();
initDropdowns();
@@ -1552,3 +1627,31 @@ function initUserProfileQrFix() {
if (checks > 12) clearInterval(interval);
}, 500);
}
/**
* OTS: Help pane fallback click handler.
* The core help-pane.js may fail to render templates in white-label themes
* (isXiboThemed = false). This handler catches the click and opens the
* help landing page directly if the core handler didn't show the container.
*/
(function() {
'use strict';
var btn = document.querySelector('#help-pane .help-pane-btn');
if (!btn) return;
btn.addEventListener('click', function() {
// Give the core handler 150ms to show the container
setTimeout(function() {
var container = document.querySelector('#help-pane .help-pane-container');
if (container && container.offsetHeight > 0 && container.innerHTML.trim() !== '') {
return; // Core handler worked — do nothing
}
// Fallback: open the help landing page directly
var pane = document.getElementById('help-pane');
var url = pane && pane.getAttribute('data-url-help-landing-page');
if (url) {
window.open(url, '_blank', 'noopener');
}
}, 150);
});
})();