Refactor filter panels and enhance sidebar functionality
- Updated filter panel toggle icons from chevron-up to chevron-down across multiple pages for consistency. - Added 'collapsed' class to filter content divs to manage visibility state. - Enhanced library page button for tidying up media items, replacing the trash icon with a custom SVG broom icon. - Improved CSS styles for sidebar and page header to ensure visibility and proper layout when the sidebar is collapsed. - Introduced JavaScript functionality to manage sidebar width and state, including theme toggle for light/dark mode. - Created a new notification drawer template that adapts based on the compact view state.
This commit is contained in:
@@ -6,6 +6,25 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Apply saved or system-preferred theme as early as possible to avoid
|
||||
// a flash from dark -> light when navigating between pages.
|
||||
(function() {
|
||||
try {
|
||||
var stored = localStorage.getItem('ots-theme-mode');
|
||||
var prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
var initial = stored || (prefersLight ? 'light' : 'light');
|
||||
if (initial === 'light') {
|
||||
document.documentElement.classList.add('ots-light-mode');
|
||||
if (document.body) document.body.classList.add('ots-light-mode');
|
||||
} else {
|
||||
document.documentElement.classList.remove('ots-light-mode');
|
||||
if (document.body) document.body.classList.remove('ots-light-mode');
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore failures (e.g. localStorage unavailable)
|
||||
}
|
||||
})();
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
sidebarCollapsed: 'otsTheme:sidebarCollapsed'
|
||||
};
|
||||
@@ -17,10 +36,181 @@
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const collapsed = sidebar.classList.contains('collapsed');
|
||||
const base = collapsed ? 88 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 240;
|
||||
// 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(88, Math.round(base + padding));
|
||||
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
|
||||
if (window.__otsDebug) {
|
||||
console.log('[OTS] updateSidebarWidth', { collapsed, base, value, cssVar: getComputedStyle(document.documentElement).getPropertyValue('--ots-sidebar-width') });
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
*/
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function updateSidebarGap() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
// target likely content containers in this app
|
||||
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');
|
||||
} catch (err) {
|
||||
pageWrapper.style.marginLeft = `${newMargin}px`;
|
||||
pageWrapper.style.paddingLeft = `${desiredInnerPadding}px`;
|
||||
}
|
||||
// Also adjust common child wrapper padding if present
|
||||
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) {}
|
||||
});
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
function debounce(fn, wait) {
|
||||
let t;
|
||||
return function () {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn.apply(this, arguments), wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflect sidebar open/collapsed state on the document body
|
||||
*/
|
||||
function updateSidebarStateClass() {
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
if (!sidebar) return;
|
||||
const body = document.body;
|
||||
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||
if (!isCollapsed) {
|
||||
body.classList.add('ots-sidebar-open');
|
||||
} else {
|
||||
body.classList.remove('ots-sidebar-open');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +219,8 @@
|
||||
function initSidebarToggle() {
|
||||
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
|
||||
const sidebar = document.querySelector('.ots-sidebar');
|
||||
const collapseBtn = document.querySelector('.sidebar-collapse-btn');
|
||||
const collapseBtn = document.querySelector('.sidebar-collapse-btn-visible');
|
||||
const expandBtn = document.querySelector('.sidebar-expand-btn');
|
||||
const body = document.body;
|
||||
|
||||
if (!sidebar) return;
|
||||
@@ -46,6 +237,8 @@
|
||||
if (isCollapsed) {
|
||||
sidebar.classList.add('collapsed');
|
||||
body.classList.add('ots-sidebar-collapsed');
|
||||
updateSidebarStateClass();
|
||||
updateSidebarGap();
|
||||
}
|
||||
|
||||
collapseBtn.addEventListener('click', function(e) {
|
||||
@@ -54,7 +247,50 @@
|
||||
sidebar.classList.toggle('collapsed');
|
||||
body.classList.toggle('ots-sidebar-collapsed', nowCollapsed);
|
||||
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
|
||||
updateSidebarWidth();
|
||||
// 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) {}
|
||||
});
|
||||
}
|
||||
|
||||
if (expandBtn) {
|
||||
expandBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
sidebar.classList.remove('collapsed');
|
||||
body.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) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,9 +302,13 @@
|
||||
|
||||
if (!isClickInsideSidebar && !isClickOnToggle && sidebar.classList.contains('active')) {
|
||||
sidebar.classList.remove('active');
|
||||
updateSidebarStateClass();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure initial state class is set
|
||||
updateSidebarStateClass();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,9 +385,35 @@
|
||||
|
||||
// Toggle menu on button click
|
||||
dropdown.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]')) {
|
||||
if (e.target.closest('.user-btn') || e.target.closest('[aria-label="User menu"]') || e.target.closest('#navbarUserMenu')) {
|
||||
e.preventDefault();
|
||||
dropdown.classList.toggle('active');
|
||||
|
||||
// If this dropdown contains the user menu, compute placement to avoid going off-screen
|
||||
const menu = dropdown.querySelector('.dropdown-menu.ots-user-menu');
|
||||
const trigger = dropdown.querySelector('#navbarUserMenu');
|
||||
if (menu && trigger) {
|
||||
// Reset any previous placement classes
|
||||
menu.classList.remove('dropdown-menu-left');
|
||||
menu.classList.remove('dropdown-menu-right');
|
||||
|
||||
// Use getBoundingClientRect for accurate placement
|
||||
const trigRect = trigger.getBoundingClientRect();
|
||||
// Ensure menu is in DOM and has an offsetWidth
|
||||
const menuWidth = menu.offsetWidth || 220; // fallback estimate
|
||||
|
||||
const spaceRight = window.innerWidth - trigRect.right;
|
||||
const spaceLeft = trigRect.left;
|
||||
|
||||
// Prefer opening to the right where possible, otherwise open to the left
|
||||
if (spaceRight < menuWidth && spaceLeft > menuWidth) {
|
||||
// not enough space on the right, open to left
|
||||
menu.classList.add('dropdown-menu-left');
|
||||
} else {
|
||||
// default to right-aligned
|
||||
menu.classList.add('dropdown-menu-right');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -318,6 +584,7 @@
|
||||
sidebar.classList.add('mobile');
|
||||
}
|
||||
updateSidebarWidth();
|
||||
updateSidebarGap();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -442,12 +709,55 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize light/dark mode toggle
|
||||
*/
|
||||
function initThemeToggle() {
|
||||
const themeToggle = document.getElementById('ots-theme-toggle');
|
||||
if (!themeToggle) return;
|
||||
|
||||
const storedTheme = localStorage.getItem('ots-theme-mode');
|
||||
const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
const effectiveTheme = storedTheme || (prefersLight ? 'light' : 'dark');
|
||||
const body = document.body;
|
||||
const root = document.documentElement;
|
||||
|
||||
// Apply stored theme on page load (apply to both <html> and <body>)
|
||||
if (effectiveTheme === 'light') {
|
||||
body.classList.add('ots-light-mode');
|
||||
root.classList.add('ots-light-mode');
|
||||
updateThemeLabel();
|
||||
}
|
||||
|
||||
// Toggle on click (keep <html> in sync so :root variables reflect mode)
|
||||
themeToggle.addEventListener('click', function(e) {
|
||||
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');
|
||||
updateThemeLabel();
|
||||
});
|
||||
|
||||
function updateThemeLabel() {
|
||||
const icon = document.getElementById('ots-theme-icon');
|
||||
const label = document.getElementById('ots-theme-label');
|
||||
const isLight = body.classList.contains('ots-light-mode');
|
||||
if (icon) {
|
||||
icon.className = isLight ? 'fa fa-sun-o' : 'fa fa-moon-o';
|
||||
}
|
||||
if (label) {
|
||||
label.textContent = isLight ? 'Light Mode' : 'Dark Mode';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all features when DOM is ready
|
||||
*/
|
||||
function init() {
|
||||
initSidebarToggle();
|
||||
initSidebarSectionToggles();
|
||||
initThemeToggle();
|
||||
initDropdowns();
|
||||
initSearch();
|
||||
initPageInteractions();
|
||||
@@ -456,7 +766,14 @@
|
||||
makeResponsive();
|
||||
initChartSafeguard();
|
||||
updateSidebarWidth();
|
||||
window.addEventListener('resize', updateSidebarWidth);
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarGap();
|
||||
var debouncedUpdate = debounce(function() {
|
||||
updateSidebarNavOffset();
|
||||
updateSidebarWidth();
|
||||
updateSidebarGap();
|
||||
}, 120);
|
||||
window.addEventListener('resize', debouncedUpdate);
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
|
||||
Reference in New Issue
Block a user