pre-img swap
This commit is contained in:
@@ -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);
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user