DotNet_Capstone_Project/.Net Capstone Project/sidebarvv.js

301 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Single source of truth for the left navigation on EVERY page.
//
// Two problems this file solves:
//
// 1. Pages that never call renderSidebar() — it self-initialises on
// DOMContentLoaded, so just including the script is enough.
//
// 2. Pages with their own custom script that renders a DIFFERENT (shorter)
// menu — a MutationObserver watches the container and re-asserts the full
// list whenever something replaces it. This is why CDS Alerts and
// Observations were showing only 10 items while the Dashboard showed all
// of them: those pages build their own nav after this script has run.
//
// Adding a nav item? Add it to SIDEBAR_ITEMS and it appears everywhere.
const SIDEBAR_ITEMS = [
{ key: 'dashboard', href: 'dashboard.html', icon: '🏠', label: 'Dashboard' },
{ key: 'patients', href: 'patients.html', icon: '🧑\u200d⚕', label: 'Patients' },
{ key: 'conditions', href: 'conditions.html', icon: '🩺', label: 'Conditions' },
{ key: 'observations', href: 'observations.html', icon: '📈', label: 'Observations' },
{ key: 'allergies', href: 'allergies.html', icon: '⚠️', label: 'Allergies' },
{ key: 'medications', href: 'medications.html', icon: '💊', label: 'Medications' },
{ key: 'encounters', href: 'encounters.html', icon: '📅', label: 'Encounters' },
{ key: 'organizations', href: 'organizations.html', icon: '🏥', label: 'Organizations' },
{ key: 'locations', href: 'locations.html', icon: '📍', label: 'Locations' },
{ key: 'practitioners', href: 'practitioners.html', icon: '👨‍⚕️', label: 'Practitioners' },
{ key: 'practitionerroles', href: 'practitionerroles.html', icon: '🩹', label: 'Practitioner Roles' },
{ key: 'servicerequests', href: 'servicerequests.html', icon: '📄', label: 'Service Requests' },
{ key: 'riskassessments', href: 'riskassessments.html', icon: '📊', label: 'Risk Assessment' },
{ key: 'cdsalerts', href: 'cdsalerts.html', icon: '🔔', label: 'CDS Alerts' },
{ key: 'careplans', href: 'careplans.html', icon: '📋', label: 'Care Plans' },
{ key: 'cdsrules', href: 'cdsrules.html', icon: '⚙️', label: 'CDS Rules' },
{ key: 'users', href: 'users.html', icon: '👤', label: 'Users' }
];
// The collapse + scroll + search styles ship with this script rather than
// css/styles.css. That external file wasn't being picked up on most pages,
// which is why the toggle only ever worked on the dashboard.
const SIDEBAR_STYLES = `
.app-sidebar {
display: flex;
flex-direction: column;
transition: width .16s ease;
/* Without a bounded height the nav grows instead of scrolling, which is
why the scrollbar never appeared. */
max-height: 100vh;
position: sticky;
top: 0;
overflow: hidden;
}
.sidebar-toggle {
background: rgba(255,255,255,.14); border: none; color: #fff;
font-size: 1.1rem; line-height: 1; width: 40px; height: 40px;
border-radius: 10px; margin: 0 .2rem .6rem; cursor: pointer; flex-shrink: 0;
}
.sidebar-toggle:hover { background: rgba(255,255,255,.26); }
/* Hamburger in the top bar, left of the brand. */
.navbar-sidebar-toggle {
background: rgba(255,255,255,.16); border: none; color: #fff;
font-size: 1.15rem; line-height: 1; width: 38px; height: 38px;
border-radius: 9px; margin-right: .75rem; cursor: pointer; flex-shrink: 0;
}
.navbar-sidebar-toggle:hover { background: rgba(255,255,255,.3); }
/* Welcome line at the top of the rail. */
.sidebar-welcome {
padding: 0 .55rem .55rem; flex-shrink: 0;
border-bottom: 1px solid rgba(255,255,255,.12); margin-bottom: .55rem;
}
.sidebar-welcome-hi { display: block; font-size: .72rem; color: rgba(255,255,255,.6); }
.sidebar-welcome-name {
display: block; font-weight: 600; color: #fff; font-size: .95rem;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* --- Search --- */
.sidebar-search { padding: 0 .35rem .6rem; flex-shrink: 0; }
.sidebar-search input {
width: 100%; border-radius: 8px; border: 1px solid rgba(255,255,255,.2);
background: rgba(255,255,255,.12); color: #fff;
padding: .35rem .6rem; font-size: .85rem;
}
.sidebar-search input::placeholder { color: rgba(255,255,255,.6); }
.sidebar-search input:focus {
outline: none; border-color: rgba(255,255,255,.5);
background: rgba(255,255,255,.18);
}
.sidebar-empty {
color: rgba(255,255,255,.55); font-size: .82rem;
padding: .5rem .6rem;
}
/* --- Scrollable nav so long menus don't get cut off --- */
.sidebar-nav {
flex: 1 1 auto;
overflow-y: auto;
min-height: 0;
padding-right: .15rem;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,.45) rgba(255,255,255,.08);
}
.sidebar-nav::-webkit-scrollbar { width: 10px; }
.sidebar-nav::-webkit-scrollbar-track { background: rgba(0,0,0,.22); border-radius: 5px; }
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255,255,255,.65); border-radius: 5px;
border: 2px solid transparent; background-clip: content-box;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover { background: #fff; background-clip: content-box; }
/* --- Collapsed rail --- */
/* !important because css/styles.css sets .app-sidebar { width: ... } and,
depending on load order, could otherwise win this. */
body.sidebar-collapsed .app-sidebar,
body.sidebar-collapsed #appSidebar {
width: 68px !important;
min-width: 68px !important;
padding-left: .4rem;
padding-right: .4rem;
}
body.sidebar-collapsed .sidebar-text { display: none; }
body.sidebar-collapsed .sidebar-search,
body.sidebar-collapsed .sidebar-welcome { display: none; }
body.sidebar-collapsed .sidebar-link { justify-content: center; padding-left: .4rem; padding-right: .4rem; }
`;
function injectSidebarStyles() {
if (document.getElementById('cip-sidebar-styles')) return;
const style = document.createElement('style');
style.id = 'cip-sidebar-styles';
style.textContent = SIDEBAR_STYLES;
document.head.appendChild(style);
}
// Pages that deliberately have no sidebar (public / auth screens).
const SIDEBAR_EXCLUDED_PAGES = ['index.html', 'login.html', 'register.html', 'callback.html'];
// Set while this script is writing to the container, so the observer below
// doesn't react to its own changes and loop forever.
let sidebarWriting = false;
function sidebarContainer() {
return document.getElementById('appSidebar') || document.querySelector('.app-sidebar');
}
// Works out which item to highlight from the URL, so a page doesn't have to
// pass its key in. An explicit key still wins when one is supplied.
function sidebarKeyFromUrl() {
const file = (window.location.pathname.split('/').pop() || 'dashboard.html').toLowerCase();
const match = SIDEBAR_ITEMS.find(i => i.href.toLowerCase() === file);
return match ? match.key : '';
}
// Puts the hamburger in the top bar, immediately left of the brand — the
// spot people reach for first. Returns false if the page has no navbar, in
// which case the button stays inside the sidebar as before.
function mountNavbarToggle() {
const brand = document.querySelector('.navbar-brand');
if (!brand) return false;
if (!document.getElementById('sidebarToggle')) {
const btn = document.createElement('button');
btn.id = 'sidebarToggle';
btn.type = 'button';
btn.className = 'navbar-sidebar-toggle';
btn.title = 'Collapse / expand menu';
btn.setAttribute('aria-label', 'Toggle navigation');
btn.textContent = '☰';
brand.parentNode.insertBefore(btn, brand);
}
return true;
}
// "Welcome, <name>" using whoever signed in. auth.js stores the display name
// under cip_clinician at sign-in.
function currentUserName() {
try {
return sessionStorage.getItem('cip_clinician')
|| localStorage.getItem('cip_clinician')
|| '';
} catch { return ''; }
}
function renderSidebar(activeKey) {
injectSidebarStyles();
const container = sidebarContainer();
if (!container) return;
const key = activeKey || sidebarKeyFromUrl();
const collapsed = localStorage.getItem('cip_sidebar_collapsed') === '1';
sidebarWriting = true;
const navbarHasToggle = mountNavbarToggle();
container.innerHTML = `
${navbarHasToggle ? '' : `
<button class="sidebar-toggle" id="sidebarToggle" type="button"
title="Collapse / expand menu" aria-label="Toggle navigation">☰</button>`}
<div class="sidebar-welcome" id="sidebarWelcome"></div>
<div class="sidebar-search">
<input type="search" id="sidebarSearch" placeholder="Search menu…" autocomplete="off">
</div>
<nav class="sidebar-nav" id="sidebarNav">
${SIDEBAR_ITEMS.map(i => `
<a href="${i.href}" class="sidebar-link ${i.key === key ? 'active' : ''}"
data-label="${i.label.toLowerCase()}" title="${i.label}">
<span class="sidebar-icon">${i.icon}</span><span class="sidebar-text">${i.label}</span>
</a>`).join('')}
<div class="sidebar-empty d-none" id="sidebarNoMatch">No matching page.</div>
</nav>
`;
const name = currentUserName();
const welcome = document.getElementById('sidebarWelcome');
if (welcome) {
welcome.innerHTML = name
? `<span class="sidebar-welcome-hi">Welcome,</span>
<span class="sidebar-welcome-name">${name}</span>`
: '';
welcome.classList.toggle('d-none', !name);
}
applySidebarState(collapsed);
// Filter the menu as you type. Expands the rail first if it's collapsed,
// since the search box is hidden in that state.
const search = document.getElementById('sidebarSearch');
if (search) search.oninput = () => {
const term = search.value.trim().toLowerCase();
let shown = 0;
container.querySelectorAll('.sidebar-link').forEach(a => {
const match = !term || a.dataset.label.includes(term);
a.classList.toggle('d-none', !match);
if (match) shown++;
});
document.getElementById('sidebarNoMatch').classList.toggle('d-none', shown > 0);
};
// Assigned, not added. The navbar button is created once and survives
// every re-render, so addEventListener stacked a new handler each time —
// an even number of handlers flipped the state twice per click, which
// looked exactly like a dead button.
const toggleBtn = document.getElementById('sidebarToggle');
if (toggleBtn) {
toggleBtn.onclick = () => {
const nowCollapsed = !document.body.classList.contains('sidebar-collapsed');
applySidebarState(nowCollapsed);
try {
localStorage.setItem('cip_sidebar_collapsed', nowCollapsed ? '1' : '0');
} catch {}
};
}
// Release on the next tick so the observer ignores this whole write.
setTimeout(() => { sidebarWriting = false; }, 0);
}
function applySidebarState(collapsed) {
document.body.classList.toggle('sidebar-collapsed', collapsed);
}
// Re-render if the menu ever ends up with the wrong number of links — which
// is what happens when a page's own script overwrites the container after
// this one has run.
function guardSidebar() {
const container = sidebarContainer();
if (!container) return;
new MutationObserver(() => {
if (sidebarWriting) return;
const links = container.querySelectorAll('.sidebar-link').length;
if (links !== SIDEBAR_ITEMS.length) renderSidebar();
}).observe(container, { childList: true, subtree: true });
}
function initSidebar() {
const file = (window.location.pathname.split('/').pop() || '').toLowerCase();
if (SIDEBAR_EXCLUDED_PAGES.includes(file)) return;
renderSidebar();
guardSidebar();
// Belt and braces for pages whose own script runs late (after images and
// stylesheets finish loading).
window.addEventListener('load', () => {
const container = sidebarContainer();
if (container && container.querySelectorAll('.sidebar-link').length !== SIDEBAR_ITEMS.length) {
renderSidebar();
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSidebar);
} else {
initSidebar();
}