diff --git a/netbox/project-static/dist/config.js b/netbox/project-static/dist/config.js index f797ea52e..14b9df04f 100644 Binary files a/netbox/project-static/dist/config.js and b/netbox/project-static/dist/config.js differ diff --git a/netbox/project-static/dist/config.js.map b/netbox/project-static/dist/config.js.map index 01bca10ef..a955a3759 100644 Binary files a/netbox/project-static/dist/config.js.map and b/netbox/project-static/dist/config.js.map differ diff --git a/netbox/project-static/dist/jobs.js b/netbox/project-static/dist/jobs.js index 6409ba900..5dce4b73c 100644 Binary files a/netbox/project-static/dist/jobs.js and b/netbox/project-static/dist/jobs.js differ diff --git a/netbox/project-static/dist/jobs.js.map b/netbox/project-static/dist/jobs.js.map index 880b13d4e..1dc717dfb 100644 Binary files a/netbox/project-static/dist/jobs.js.map and b/netbox/project-static/dist/jobs.js.map differ diff --git a/netbox/project-static/dist/lldp.js b/netbox/project-static/dist/lldp.js index 7092bd1c8..48a1ee094 100644 Binary files a/netbox/project-static/dist/lldp.js and b/netbox/project-static/dist/lldp.js differ diff --git a/netbox/project-static/dist/lldp.js.map b/netbox/project-static/dist/lldp.js.map index 1509a6a67..615a15440 100644 Binary files a/netbox/project-static/dist/lldp.js.map and b/netbox/project-static/dist/lldp.js.map differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 1d2edfa38..82323dc9b 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 1a5b9f1ee..6c7dc5fd0 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/dist/status.js b/netbox/project-static/dist/status.js index 65ae99fbf..a640a92d1 100644 Binary files a/netbox/project-static/dist/status.js and b/netbox/project-static/dist/status.js differ diff --git a/netbox/project-static/dist/status.js.map b/netbox/project-static/dist/status.js.map index 2f7590fb5..864a28efa 100644 Binary files a/netbox/project-static/dist/status.js.map and b/netbox/project-static/dist/status.js.map differ diff --git a/netbox/project-static/src/bs.ts b/netbox/project-static/src/bs.ts index 58216592a..ee22bf685 100644 --- a/netbox/project-static/src/bs.ts +++ b/netbox/project-static/src/bs.ts @@ -1,4 +1,4 @@ -import { Modal, Tab, Toast, Tooltip } from 'bootstrap'; +import { Collapse, Modal, Tab, Toast, Tooltip } from 'bootstrap'; import Masonry from 'masonry-layout'; import { getElements } from './util'; @@ -6,6 +6,7 @@ type ToastLevel = 'danger' | 'warning' | 'success' | 'info'; // Add common Bootstrap components to `window`, so they may be consumed globally (primarily for // plugins). +window.Collapse = Collapse; window.Modal = Modal; window.Toast = Toast; window.Tooltip = Tooltip; diff --git a/netbox/project-static/src/global.d.ts b/netbox/project-static/src/global.d.ts index dbe63eae8..e0a52735a 100644 --- a/netbox/project-static/src/global.d.ts +++ b/netbox/project-static/src/global.d.ts @@ -7,6 +7,11 @@ type Dict = Record; type Nullable = T | null; interface Window { + /** + * Bootstrap Collapse Instance. + */ + Collapse: typeof import('bootstrap').Collapse; + /** * Bootstrap Modal Instance. */ diff --git a/netbox/project-static/src/sidenav.ts b/netbox/project-static/src/sidenav.ts index 90f0623ae..964b73c95 100644 --- a/netbox/project-static/src/sidenav.ts +++ b/netbox/project-static/src/sidenav.ts @@ -1,8 +1,10 @@ +import { Collapse } from 'bootstrap'; import { StateManager } from './state'; import { getElements, isElement } from './util'; type NavState = { pinned: boolean }; type BodyAttr = 'show' | 'hide' | 'hidden' | 'pinned'; +type Section = [HTMLAnchorElement, InstanceType]; class SideNav { /** @@ -15,6 +17,16 @@ class SideNav { */ private state: StateManager; + /** + * The currently active parent nav-link controlling a section. + */ + private activeLink: Nullable = null; + + /** + * All collapsible sections and their controlling nav-links. + */ + private sections: Section[] = []; + constructor(base: HTMLDivElement) { this.base = base; this.state = new StateManager( @@ -23,6 +35,7 @@ class SideNav { ); this.init(); + this.initSectionLinks(); this.initLinks(); } @@ -97,11 +110,17 @@ class SideNav { } } + /** + * Show the sidenav. + */ private show(): void { this.bodyAdd('show'); this.bodyRemove('hidden', 'hide'); } + /** + * Hide the sidenav and collapse all active nav sections. + */ private hide(): void { this.bodyAdd('hidden'); this.bodyRemove('pinned', 'show'); @@ -131,6 +150,51 @@ class SideNav { this.state.set('pinned', false); } + /** + * When a section's controlling nav-link is clicked, update this instance's `activeLink` + * attribute and close all other sections. + */ + private handleSectionClick(event: Event): void { + event.preventDefault(); + const element = event.target as HTMLAnchorElement; + this.activeLink = element; + this.closeInactiveSections(); + } + + /** + * Close all sections that are not associated with the currently active link (`activeLink`). + */ + private closeInactiveSections(): void { + for (const [link, collapse] of this.sections) { + if (link !== this.activeLink) { + link.classList.add('collapsed'); + link.setAttribute('aria-expanded', 'false'); + collapse.hide(); + } + } + } + + /** + * Initialize `bootstrap.Collapse` instances on all section collapse elements and add event + * listeners to the controlling nav-links. + */ + private initSectionLinks(): void { + for (const section of getElements( + '.navbar-nav .nav-item .nav-link[data-bs-toggle]', + )) { + if (section.parentElement !== null) { + const collapse = section.parentElement.querySelector('.collapse'); + if (collapse !== null) { + const collapseInstance = new Collapse(collapse, { + toggle: false, // Don't automatically open the collapse element on invocation. + }); + this.sections.push([section, collapseInstance]); + section.addEventListener('click', event => this.handleSectionClick(event)); + } + } + } + } + /** * Starting from the bottom-most active link in the element tree, work backwards to determine the * link's containing `.collapse` element and the `.collapse` element's containing `.nav-link` @@ -232,6 +296,10 @@ class SideNav { } } + /** + * Handle sidenav visibility state for small screens. On small screens, there is no pinned state, + * only open/closed. + */ private onMobileToggle(event: Event): void { event.preventDefault(); if (this.bodyHas('hidden')) { diff --git a/netbox/project-static/styles/sidenav.scss b/netbox/project-static/styles/sidenav.scss index 3363b1f41..0c7bbfb1e 100644 --- a/netbox/project-static/styles/sidenav.scss +++ b/netbox/project-static/styles/sidenav.scss @@ -271,10 +271,6 @@ white-space: nowrap; transition: $transition-100ms-ease-in-out; - // &.disabled { - // opacity: 0.8; - // } - &.active { position: relative; color: var(--nbx-sidebar-link-hover-bg);