Upload files to ".Net Capstone Project"
This commit is contained in:
parent
2274719221
commit
9a35f9e52d
300
.Net Capstone Project/sidebarvv.js
Normal file
300
.Net Capstone Project/sidebarvv.js
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user