2026-02-04 06:23:04 -05:00
|
|
|
/**
|
|
|
|
|
* OTS Signage Modern Theme - Client-Side Utilities
|
2026-02-04 07:17:33 -05:00
|
|
|
* Sidebar toggle, dropdown menus, and UI interactions
|
2026-02-04 06:23:04 -05:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const STORAGE_KEYS = {
|
2026-02-04 07:17:33 -05:00
|
|
|
sidebarCollapsed: 'otsTheme:sidebarCollapsed'
|
2026-02-04 06:23:04 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize sidebar toggle functionality
|
|
|
|
|
*/
|
|
|
|
|
function initSidebarToggle() {
|
|
|
|
|
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
|
2026-02-04 07:17:33 -05:00
|
|
|
const sidebar = document.querySelector('.ots-sidebar');
|
2026-02-04 15:26:44 -05:00
|
|
|
const closeBtn = document.querySelector('.ots-sidebar-close');
|
|
|
|
|
const collapseBtn = document.querySelector('.sidebar-collapse-btn');
|
|
|
|
|
const body = document.body;
|
2026-02-04 06:23:04 -05:00
|
|
|
|
2026-02-04 15:26:44 -05:00
|
|
|
if (!sidebar) return;
|
2026-02-04 06:23:04 -05:00
|
|
|
|
2026-02-04 15:26:44 -05:00
|
|
|
// Handle sidebar close button
|
|
|
|
|
if (closeBtn) {
|
|
|
|
|
closeBtn.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
sidebar.classList.remove('active');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (toggleBtn) {
|
|
|
|
|
toggleBtn.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
sidebar.classList.toggle('active');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (collapseBtn) {
|
|
|
|
|
const isCollapsed = localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === 'true';
|
|
|
|
|
if (isCollapsed) {
|
|
|
|
|
sidebar.classList.add('collapsed');
|
|
|
|
|
body.classList.add('ots-sidebar-collapsed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
collapseBtn.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const nowCollapsed = !sidebar.classList.contains('collapsed');
|
|
|
|
|
sidebar.classList.toggle('collapsed');
|
|
|
|
|
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
|
|
|
|
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize sidebar section toggles
|
|
|
|
|
initSidebarSectionToggles();
|
2026-02-04 07:17:33 -05:00
|
|
|
|
|
|
|
|
// Close sidebar when clicking outside on mobile
|
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
|
if (window.innerWidth <= 768) {
|
|
|
|
|
const isClickInsideSidebar = sidebar.contains(e.target);
|
2026-02-04 15:26:44 -05:00
|
|
|
const isClickOnToggle = toggleBtn && toggleBtn.contains(e.target);
|
2026-02-04 07:17:33 -05:00
|
|
|
|
|
|
|
|
if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
|
|
|
|
|
sidebar.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-04 06:23:04 -05:00
|
|
|
|
2026-02-04 15:26:44 -05:00
|
|
|
/**
|
|
|
|
|
* Initialize sidebar section collapse/expand functionality
|
|
|
|
|
*/
|
|
|
|
|
function initSidebarSectionToggles() {
|
|
|
|
|
const groupToggles = document.querySelectorAll('.sidebar-group-toggle');
|
|
|
|
|
|
|
|
|
|
groupToggles.forEach(toggle => {
|
|
|
|
|
toggle.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const group = toggle.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);
|
|
|
|
|
toggle.setAttribute('aria-expanded', (!isOpen).toString());
|
|
|
|
|
submenu.style.display = isOpen ? 'none' : 'block';
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 07:17:33 -05:00
|
|
|
/**
|
|
|
|
|
* Initialize dropdown menus
|
|
|
|
|
*/
|
|
|
|
|
function initDropdowns() {
|
|
|
|
|
const dropdowns = document.querySelectorAll('.dropdown');
|
2026-02-04 15:26:44 -05:00
|
|
|
|
2026-02-04 07:17:33 -05:00
|
|
|
dropdowns.forEach(dropdown => {
|
2026-02-04 15:26:44 -05:00
|
|
|
const toggle = dropdown.querySelector('.dropdown-toggle, [data-toggle="dropdown"], .dt-button');
|
2026-02-04 07:17:33 -05:00
|
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
2026-02-04 15:26:44 -05:00
|
|
|
|
|
|
|
|
if (!toggle || !menu) return;
|
|
|
|
|
|
|
|
|
|
// Toggle menu on toggle click
|
|
|
|
|
toggle.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
dropdown.classList.toggle('active');
|
2026-02-04 07:17:33 -05:00
|
|
|
});
|
2026-02-04 15:26:44 -05:00
|
|
|
|
2026-02-04 07:17:33 -05:00
|
|
|
// Close menu when clicking outside
|
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
|
if (!dropdown.contains(e.target)) {
|
|
|
|
|
dropdown.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-02-04 06:23:04 -05:00
|
|
|
});
|
2026-02-04 15:26:44 -05:00
|
|
|
// Support DataTables Buttons collections which are not wrapped by .dropdown
|
|
|
|
|
document.addEventListener('click', function(e) {
|
|
|
|
|
const btn = e.target.closest('.dt-button');
|
|
|
|
|
|
|
|
|
|
if (btn) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
const wrapper = btn.closest('.dt-buttons') || btn.parentElement;
|
|
|
|
|
|
|
|
|
|
// close other open dt-buttons collections
|
|
|
|
|
document.querySelectorAll('.dt-buttons.active').forEach(w => {
|
|
|
|
|
if (w !== wrapper) w.classList.remove('active');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
wrapper.classList.toggle('active');
|
|
|
|
|
|
|
|
|
|
// If DataTables placed the collection on the body, find it and position it under the clicked button
|
|
|
|
|
const allCollections = Array.from(document.querySelectorAll('.dt-button-collection'));
|
|
|
|
|
let collection = wrapper.querySelector('.dt-button-collection') || allCollections.find(c => !wrapper.contains(c));
|
|
|
|
|
|
|
|
|
|
// If DataTables didn't create a collection element, create one as a fallback
|
|
|
|
|
if (!collection) {
|
|
|
|
|
collection = document.createElement('div');
|
|
|
|
|
collection.className = 'dt-button-collection';
|
|
|
|
|
// prefer to append near wrapper for positioning; fallback to body
|
|
|
|
|
(wrapper || document.body).appendChild(collection);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (collection) {
|
|
|
|
|
// hide other collections
|
|
|
|
|
allCollections.forEach(c => { if (c !== collection) { c.classList.remove('show'); c.style.display = 'none'; } });
|
|
|
|
|
|
|
|
|
|
const rect = btn.getBoundingClientRect();
|
|
|
|
|
const top = rect.bottom + window.scrollY;
|
|
|
|
|
const left = rect.left + window.scrollX;
|
|
|
|
|
|
|
|
|
|
collection.style.position = 'absolute';
|
|
|
|
|
collection.style.top = `${top}px`;
|
|
|
|
|
collection.style.left = `${left}px`;
|
|
|
|
|
collection.style.display = 'block';
|
|
|
|
|
collection.classList.add('show');
|
|
|
|
|
// DEBUG: log collection contents
|
|
|
|
|
try {
|
|
|
|
|
console.log('dt-button-collection opened, children:', collection.children.length, collection);
|
|
|
|
|
} catch (err) {}
|
|
|
|
|
|
|
|
|
|
// If the collection is empty or visually empty, build a fallback column list from the nearest table
|
|
|
|
|
const isEmpty = collection.children.length === 0 || collection.textContent.trim() === '' || collection.offsetHeight < 10;
|
|
|
|
|
if (isEmpty) {
|
|
|
|
|
try {
|
|
|
|
|
let table = btn.closest('table') || wrapper.querySelector('table') || document.querySelector('table');
|
|
|
|
|
if (table && window.jQuery && jQuery.fn && jQuery.fn.dataTable && jQuery.fn.dataTable.isDataTable(table)) {
|
|
|
|
|
const dt = jQuery(table).DataTable();
|
|
|
|
|
// clear existing
|
|
|
|
|
collection.innerHTML = '';
|
|
|
|
|
const thead = table.querySelectorAll('thead th');
|
|
|
|
|
thead.forEach((th, idx) => {
|
|
|
|
|
const text = (th.textContent || th.innerText || `Column ${idx+1}`).trim();
|
|
|
|
|
const item = document.createElement('div');
|
|
|
|
|
item.style.padding = '6px 12px';
|
|
|
|
|
item.style.display = 'flex';
|
|
|
|
|
item.style.alignItems = 'center';
|
|
|
|
|
item.style.gap = '8px';
|
|
|
|
|
const checkbox = document.createElement('input');
|
|
|
|
|
checkbox.type = 'checkbox';
|
|
|
|
|
checkbox.checked = dt.column(idx).visible();
|
|
|
|
|
checkbox.addEventListener('change', function() {
|
|
|
|
|
dt.column(idx).visible(this.checked);
|
|
|
|
|
});
|
|
|
|
|
const label = document.createElement('span');
|
|
|
|
|
label.textContent = text;
|
|
|
|
|
label.style.color = 'var(--color-text-primary)';
|
|
|
|
|
item.appendChild(checkbox);
|
|
|
|
|
item.appendChild(label);
|
|
|
|
|
collection.appendChild(item);
|
|
|
|
|
});
|
|
|
|
|
console.log('Fallback: populated collection with', collection.children.length, 'items');
|
|
|
|
|
} else {
|
|
|
|
|
console.log('Fallback: no DataTable instance found to populate column visibility');
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('Error building fallback column list', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// click outside dt-button -> close any open collections
|
|
|
|
|
document.querySelectorAll('.dt-buttons.active').forEach(w => w.classList.remove('active'));
|
|
|
|
|
});
|
2026-02-04 06:23:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-04 07:17:33 -05:00
|
|
|
* Initialize search functionality
|
|
|
|
|
*/
|
|
|
|
|
function initSearch() {
|
|
|
|
|
const searchForm = document.querySelector('.topbar-search');
|
|
|
|
|
if (!searchForm) return;
|
|
|
|
|
|
|
|
|
|
const input = searchForm.querySelector('.search-input');
|
|
|
|
|
if (input) {
|
|
|
|
|
input.addEventListener('focus', function() {
|
|
|
|
|
searchForm.style.borderColor = 'var(--color-primary)';
|
|
|
|
|
});
|
|
|
|
|
input.addEventListener('blur', function() {
|
|
|
|
|
searchForm.style.borderColor = 'var(--color-border)';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize page specific interactions
|
2026-02-04 06:23:04 -05:00
|
|
|
*/
|
2026-02-04 07:17:33 -05:00
|
|
|
function initPageInteractions() {
|
|
|
|
|
// Displays page - folder selection
|
|
|
|
|
const folderItems = document.querySelectorAll('.folder-item');
|
|
|
|
|
folderItems.forEach(item => {
|
|
|
|
|
item.addEventListener('click', function() {
|
|
|
|
|
folderItems.forEach(f => f.classList.remove('active'));
|
|
|
|
|
this.classList.add('active');
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-02-04 06:23:04 -05:00
|
|
|
|
2026-02-04 07:17:33 -05:00
|
|
|
// Media page - item selection
|
|
|
|
|
const mediaItems = document.querySelectorAll('.media-item');
|
|
|
|
|
mediaItems.forEach(item => {
|
|
|
|
|
item.addEventListener('click', function() {
|
|
|
|
|
this.style.opacity = '0.7';
|
|
|
|
|
setTimeout(() => this.style.opacity = '1', 200);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-04 06:23:04 -05:00
|
|
|
|
2026-02-04 07:17:33 -05:00
|
|
|
/**
|
|
|
|
|
* Make sidebar responsive
|
|
|
|
|
*/
|
|
|
|
|
function makeResponsive() {
|
|
|
|
|
const sidebar = document.querySelector('.ots-sidebar');
|
|
|
|
|
const main = document.querySelector('.ots-main');
|
|
|
|
|
|
|
|
|
|
if (!sidebar) return;
|
|
|
|
|
|
|
|
|
|
// Add toggle button for mobile
|
|
|
|
|
if (window.innerWidth <= 768) {
|
|
|
|
|
sidebar.classList.add('mobile');
|
2026-02-04 06:23:04 -05:00
|
|
|
}
|
2026-02-04 07:17:33 -05:00
|
|
|
|
|
|
|
|
window.addEventListener('resize', function() {
|
|
|
|
|
if (window.innerWidth > 768) {
|
|
|
|
|
sidebar.classList.remove('mobile', 'active');
|
|
|
|
|
} else {
|
|
|
|
|
sidebar.classList.add('mobile');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-04 10:00:40 -05:00
|
|
|
* Prevent Chart.js errors when chart elements are missing
|
2026-02-04 07:17:33 -05:00
|
|
|
*/
|
2026-02-04 10:00:40 -05:00
|
|
|
function initChartSafeguard() {
|
|
|
|
|
if (!window.Chart) return;
|
|
|
|
|
|
|
|
|
|
if (typeof window.Chart.acquireContext === 'function') {
|
|
|
|
|
window.Chart.acquireContext = function(item) {
|
|
|
|
|
if (!item) return null;
|
|
|
|
|
|
|
|
|
|
const candidate = item.length ? item[0] : item;
|
|
|
|
|
if (candidate && typeof candidate.getContext === 'function') {
|
|
|
|
|
return candidate.getContext('2d');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (window.Chart.prototype && typeof window.Chart.prototype.acquireContext === 'function') {
|
|
|
|
|
window.Chart.prototype.acquireContext = function(item) {
|
|
|
|
|
if (!item) return null;
|
|
|
|
|
|
|
|
|
|
const candidate = item.length ? item[0] : item;
|
|
|
|
|
if (candidate && typeof candidate.getContext === 'function') {
|
|
|
|
|
return candidate.getContext('2d');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-02-04 07:17:33 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 10:00:40 -05:00
|
|
|
/**
|
|
|
|
|
* Enhance tables: wrap in card, add per-table search box, client-side filtering
|
|
|
|
|
* Non-destructive: skips tables already enhanced
|
|
|
|
|
*/
|
|
|
|
|
function enhanceTables() {
|
|
|
|
|
const selector = '.ots-content table, .content table, .container table, .card table, table';
|
|
|
|
|
const tables = Array.from(document.querySelectorAll(selector));
|
|
|
|
|
let counter = 0;
|
|
|
|
|
|
|
|
|
|
tables.forEach(table => {
|
|
|
|
|
// only enhance tables that have a thead and tbody
|
|
|
|
|
if (!table || table.classList.contains('modern-table')) return;
|
|
|
|
|
if (!table.querySelector('thead') || !table.querySelector('tbody')) return;
|
|
|
|
|
|
|
|
|
|
counter += 1;
|
|
|
|
|
table.classList.add('modern-table');
|
|
|
|
|
|
|
|
|
|
// Build wrapper structure
|
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
|
wrapper.className = 'modern-table-card';
|
|
|
|
|
|
|
|
|
|
const controls = document.createElement('div');
|
|
|
|
|
controls.className = 'table-controls';
|
|
|
|
|
|
|
|
|
|
const input = document.createElement('input');
|
|
|
|
|
input.type = 'search';
|
|
|
|
|
input.placeholder = 'Search…';
|
|
|
|
|
input.className = 'table-search-input';
|
|
|
|
|
input.setAttribute('aria-label', 'Table search');
|
|
|
|
|
input.style.minWidth = '180px';
|
|
|
|
|
|
|
|
|
|
controls.appendChild(input);
|
|
|
|
|
|
|
|
|
|
const tableWrapper = document.createElement('div');
|
|
|
|
|
tableWrapper.className = 'table-wrapper';
|
|
|
|
|
tableWrapper.style.overflow = 'auto';
|
|
|
|
|
|
|
|
|
|
// Insert wrapper into DOM in place of the table
|
|
|
|
|
const parent = table.parentNode;
|
|
|
|
|
parent.replaceChild(wrapper, table);
|
|
|
|
|
wrapper.appendChild(controls);
|
|
|
|
|
wrapper.appendChild(tableWrapper);
|
|
|
|
|
tableWrapper.appendChild(table);
|
|
|
|
|
|
|
|
|
|
// Simple, light-weight search filtering for this table only
|
|
|
|
|
input.addEventListener('input', function (e) {
|
|
|
|
|
const term = (e.target.value || '').toLowerCase();
|
|
|
|
|
table.querySelectorAll('tbody tr').forEach(tr => {
|
|
|
|
|
const text = tr.textContent.toLowerCase();
|
|
|
|
|
tr.style.display = term === '' || text.includes(term) ? '' : 'none';
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-02-04 07:17:33 -05:00
|
|
|
}
|
2026-02-04 06:23:04 -05:00
|
|
|
|
2026-02-04 10:00:40 -05:00
|
|
|
/**
|
|
|
|
|
* Initialize DataTables for enhanced behavior when available.
|
|
|
|
|
* Falls back gracefully if DataTables or jQuery are not present.
|
|
|
|
|
*/
|
|
|
|
|
function initDataTables() {
|
|
|
|
|
if (!window.jQuery) return;
|
|
|
|
|
const $ = window.jQuery;
|
|
|
|
|
if (!$.fn || !$.fn.dataTable) return;
|
2026-02-04 06:23:04 -05:00
|
|
|
|
2026-02-04 10:00:40 -05:00
|
|
|
$('.modern-table, table').each(function () {
|
|
|
|
|
try {
|
|
|
|
|
if (!$.fn.dataTable.isDataTable(this)) {
|
|
|
|
|
$(this).DataTable({
|
|
|
|
|
responsive: true,
|
|
|
|
|
lengthChange: false,
|
|
|
|
|
pageLength: 10,
|
|
|
|
|
autoWidth: false,
|
|
|
|
|
dom: '<"table-controls"f>rt<"table-meta"ip>',
|
|
|
|
|
language: { search: '' }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// If initialization fails, ignore and allow fallback enhancer
|
|
|
|
|
console.warn('DataTables init failed for table', this, err);
|
|
|
|
|
}
|
2026-02-04 06:23:04 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-04 10:00:40 -05:00
|
|
|
* Initialize all features when DOM is ready
|
2026-02-04 06:23:04 -05:00
|
|
|
*/
|
|
|
|
|
function init() {
|
|
|
|
|
initSidebarToggle();
|
2026-02-04 10:00:40 -05:00
|
|
|
initDropdowns();
|
|
|
|
|
initSearch();
|
|
|
|
|
initPageInteractions();
|
|
|
|
|
initDataTables();
|
|
|
|
|
enhanceTables();
|
|
|
|
|
makeResponsive();
|
|
|
|
|
initChartSafeguard();
|
2026-02-04 06:23:04 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 10:00:40 -05:00
|
|
|
// Wait for DOM to be ready
|
2026-02-04 06:23:04 -05:00
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
} else {
|
|
|
|
|
init();
|
|
|
|
|
}
|
|
|
|
|
})();
|