@ngx-smart/ngx-smart-sidebar
Version:
A customizable, collapsible, responsive, feature-rich sidebar component for Angular apps.
380 lines (370 loc) • 18.8 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, EventEmitter, Component, Input, Output, HostListener } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import * as i3 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i2 from '@angular/router';
import { NavigationEnd, RouterModule } from '@angular/router';
class SidebarService {
sidebarStates = {};
getSidebarState(id) {
if (!this.sidebarStates[id]) {
this.sidebarStates[id] = new BehaviorSubject({
isCollapsed: false,
position: 'left',
});
}
return this.sidebarStates[id].asObservable();
}
setSidebarState(id, state) {
if (!this.sidebarStates[id]) {
this.sidebarStates[id] = new BehaviorSubject({
isCollapsed: false,
position: 'left',
});
}
const currentState = this.sidebarStates[id].value;
this.sidebarStates[id].next({ ...currentState, ...state });
}
toggleSidebar(id) {
const currentState = this.sidebarStates[id]?.value;
if (currentState) {
this.sidebarStates[id].next({
...currentState,
isCollapsed: !currentState.isCollapsed,
});
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SidebarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SidebarService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SidebarService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class SidebarComponent {
sidebarService;
router;
elementRef;
position = 'left';
width = 'medium';
title = '';
backgroundHighlightColor = '#e9ecef';
items = [];
activeItemPath = '';
sidebarId = '';
collapsible = true;
hideSidebarOnPathChange = false;
closeOnClickOutside = false;
sidebarBackgroundColor = '#ffffff';
sidebarItemSelected = new EventEmitter();
toggleSidebar = new EventEmitter();
isCollapsed = false;
hoverItemIndex = null; // corresponds to i
hoverLabelIndex = null; // corresponds to j
hoverChildIndex = null; // corresponds to k
stateSubscription;
constructor(sidebarService, router, elementRef) {
this.sidebarService = sidebarService;
this.router = router;
this.elementRef = elementRef;
}
ngOnInit() {
this.sidebarService.setSidebarState(this.sidebarId, {
isCollapsed: this.isCollapsed,
position: this.position,
});
this.items?.forEach((section) => {
section.labels?.forEach((label) => {
if (label.children) {
label.isExpanded = true;
}
});
});
this.stateSubscription = this.sidebarService
.getSidebarState(this.sidebarId)
.subscribe((state) => {
this.isCollapsed = state.isCollapsed;
this.position = state.position;
});
if (this.hideSidebarOnPathChange) {
this.stateSubscription.add(this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.sidebarService.setSidebarState(this.sidebarId, {
isCollapsed: true,
});
}
}));
}
}
ngOnDestroy() {
this.stateSubscription?.unsubscribe();
}
onClickOutside(event) {
if (!this.closeOnClickOutside || this.isCollapsed) {
return;
}
const clickedInside = this.elementRef.nativeElement.contains(event.target);
if (!clickedInside) {
this.sidebarService.setSidebarState(this.sidebarId, {
isCollapsed: true,
});
}
}
toggleSidebarState() {
if (!this.collapsible)
return;
this.sidebarService.toggleSidebar(this.sidebarId);
this.toggleSidebar.emit(this.isCollapsed);
}
onItemClick(item, event) {
if (item.children) {
item.isExpanded = !item.isExpanded;
event.preventDefault();
}
else if (item.path) {
this.sidebarItemSelected.emit(item);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SidebarComponent, deps: [{ token: SidebarService }, { token: i2.Router }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: SidebarComponent, isStandalone: true, selector: "lib-sidebar", inputs: { position: "position", width: "width", title: "title", backgroundHighlightColor: "backgroundHighlightColor", items: "items", activeItemPath: "activeItemPath", sidebarId: "sidebarId", collapsible: "collapsible", hideSidebarOnPathChange: "hideSidebarOnPathChange", closeOnClickOutside: "closeOnClickOutside", sidebarBackgroundColor: "sidebarBackgroundColor" }, outputs: { sidebarItemSelected: "sidebarItemSelected", toggleSidebar: "toggleSidebar" }, host: { listeners: { "document:click": "onClickOutside($event)" } }, ngImport: i0, template: ` <nav
class="sidebar"
[ngClass]="[
position === 'left' ? 'left' : 'right',
width === 'small' ? 'small' : width === 'medium' ? 'medium' : 'large',
isCollapsed ? 'collapsed' : ''
]"
[ngStyle]="{
'background-color': sidebarBackgroundColor
? sidebarBackgroundColor
: 'transparent'
}"
>
<ng-content
class="sidebar-content-wrapper"
select="[sidebarHeaderContent]"
></ng-content>
<header *ngIf="title || collapsible" class="sidebar-header">
<h3 *ngIf="title">{{ title }}</h3>
<button
*ngIf="collapsible"
class="toggle-btn"
(click)="toggleSidebarState()"
>
{{ isCollapsed ? '☰' : '✕' }}
</button>
</header>
<ul class="sidebar-menu" *ngIf="items?.length">
<ng-container *ngFor="let item of items; let i = index">
<div class="labels-list">
<li class="section-header">
{{ item.header }}
</li>
<li
*ngFor="let label of item.labels; let j = index"
class="menu-item"
[class.active]="activeItemPath === label.path"
[class.has-children]="label.children"
>
<a
(click)="onItemClick(label, $event)"
[routerLink]="label.path || null"
[ngStyle]="{
'background-color':
hoverItemIndex === i && hoverLabelIndex === j
? backgroundHighlightColor
: 'transparent'
}"
(mouseenter)="hoverItemIndex = i; hoverLabelIndex = j"
(mouseleave)="hoverItemIndex = null; hoverLabelIndex = null"
>
<span
*ngIf="label.children?.length"
class="caret"
[class.rotated]="label.isExpanded !== false"
>
{{ label.isExpanded !== false ? '⌄' : '›' }}
</span>
<span>{{ label.label }}</span>
</a>
<ul
*ngIf="label.children && label.isExpanded !== false"
class="submenu"
role="menu"
>
<li
*ngFor="let child of label.children; let k = index"
[class.active]="activeItemPath === child.path"
class="submenu-item"
>
<a
[routerLink]="child.path"
(click)="sidebarItemSelected.emit(child)"
[ngStyle]="{
'background-color':
hoverItemIndex === i &&
hoverLabelIndex === j &&
hoverChildIndex === k
? backgroundHighlightColor
: 'transparent'
}"
(mouseenter)="
hoverItemIndex = i; hoverLabelIndex = j; hoverChildIndex = k
"
(mouseleave)="hoverChildIndex = null"
>
{{ child.label }}
</a>
</li>
</ul>
</li>
</div>
</ng-container>
</ul>
<ng-content
class="sidebar-content-wrapper"
select="[sidebarFooterContent]"
></ng-content>
</nav>`, isInline: true, styles: [":host{display:block}.sidebar{transition:width .3s ease,transform .3s ease;height:100vh;overflow-y:auto;position:fixed;top:0;background-color:#fff;z-index:1000;padding:30px 12px;word-break:break-word}.sidebar.collapsed{width:40px!important}.sidebar.collapsed .sidebar-header{justify-content:center;padding:12px 0}.sidebar.collapsed .sidebar-header h3{display:none}.sidebar.collapsed :is(.sidebar-header h3,.menu-item span,.submenu,.caret,.section-header){display:none}.sidebar.left{left:0}.sidebar.right{right:0}.sidebar.small{width:150px}.sidebar.medium{width:250px}.sidebar.large{width:300px}.sidebar-content-wrapper{display:flex;align-items:center;padding:16px 0;font-size:14px;color:#132644;font-weight:600}.sidebar-header{display:flex;justify-content:space-between;padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid #e0e0e0}.sidebar-header h3{margin:0;font-size:16px;color:#333;font-weight:600}.sidebar-header .toggle-btn{background:none;border:none;font-size:16px;cursor:pointer;color:#666;display:block!important}.sidebar-menu{list-style:none;padding:0;margin:0}.sidebar-menu .labels-list{margin-bottom:20px}.sidebar-menu .section-header{margin-bottom:4px;font-size:14px;color:#132644;font-weight:700;text-transform:uppercase;display:flex;align-items:center}.sidebar-menu .has-children{display:flex;flex-direction:column;gap:2px}.sidebar-menu .menu-item.active a{color:#007bff;font-weight:700}.sidebar-menu .menu-item a{display:flex;align-items:center;padding:10px;text-decoration:none;font-size:12px;font-weight:600;color:#132644;transition:background-color .2s}.sidebar-menu .menu-item a .caret{margin-right:10px;font-size:12px;font-weight:500;color:#757575;transition:transform .3s ease}.sidebar-menu .menu-item a .caret span{font-size:16px;font-weight:600}.sidebar-menu .submenu{padding:0;list-style:none;margin:0 0 16px 16px;border-left:2px solid #e0e0e0}.sidebar-menu .submenu .submenu-item{margin:0}.sidebar-menu .submenu .submenu-item.active a{color:#007bff;font-weight:700}.sidebar-menu .submenu .submenu-item a{padding:8px 0 8px 20px;display:block;color:#5e6e87;font-size:12px;font-weight:700;transition:all .3s ease}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SidebarComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-sidebar', standalone: true, imports: [CommonModule, RouterModule], template: ` <nav
class="sidebar"
[ngClass]="[
position === 'left' ? 'left' : 'right',
width === 'small' ? 'small' : width === 'medium' ? 'medium' : 'large',
isCollapsed ? 'collapsed' : ''
]"
[ngStyle]="{
'background-color': sidebarBackgroundColor
? sidebarBackgroundColor
: 'transparent'
}"
>
<ng-content
class="sidebar-content-wrapper"
select="[sidebarHeaderContent]"
></ng-content>
<header *ngIf="title || collapsible" class="sidebar-header">
<h3 *ngIf="title">{{ title }}</h3>
<button
*ngIf="collapsible"
class="toggle-btn"
(click)="toggleSidebarState()"
>
{{ isCollapsed ? '☰' : '✕' }}
</button>
</header>
<ul class="sidebar-menu" *ngIf="items?.length">
<ng-container *ngFor="let item of items; let i = index">
<div class="labels-list">
<li class="section-header">
{{ item.header }}
</li>
<li
*ngFor="let label of item.labels; let j = index"
class="menu-item"
[class.active]="activeItemPath === label.path"
[class.has-children]="label.children"
>
<a
(click)="onItemClick(label, $event)"
[routerLink]="label.path || null"
[ngStyle]="{
'background-color':
hoverItemIndex === i && hoverLabelIndex === j
? backgroundHighlightColor
: 'transparent'
}"
(mouseenter)="hoverItemIndex = i; hoverLabelIndex = j"
(mouseleave)="hoverItemIndex = null; hoverLabelIndex = null"
>
<span
*ngIf="label.children?.length"
class="caret"
[class.rotated]="label.isExpanded !== false"
>
{{ label.isExpanded !== false ? '⌄' : '›' }}
</span>
<span>{{ label.label }}</span>
</a>
<ul
*ngIf="label.children && label.isExpanded !== false"
class="submenu"
role="menu"
>
<li
*ngFor="let child of label.children; let k = index"
[class.active]="activeItemPath === child.path"
class="submenu-item"
>
<a
[routerLink]="child.path"
(click)="sidebarItemSelected.emit(child)"
[ngStyle]="{
'background-color':
hoverItemIndex === i &&
hoverLabelIndex === j &&
hoverChildIndex === k
? backgroundHighlightColor
: 'transparent'
}"
(mouseenter)="
hoverItemIndex = i; hoverLabelIndex = j; hoverChildIndex = k
"
(mouseleave)="hoverChildIndex = null"
>
{{ child.label }}
</a>
</li>
</ul>
</li>
</div>
</ng-container>
</ul>
<ng-content
class="sidebar-content-wrapper"
select="[sidebarFooterContent]"
></ng-content>
</nav>`, styles: [":host{display:block}.sidebar{transition:width .3s ease,transform .3s ease;height:100vh;overflow-y:auto;position:fixed;top:0;background-color:#fff;z-index:1000;padding:30px 12px;word-break:break-word}.sidebar.collapsed{width:40px!important}.sidebar.collapsed .sidebar-header{justify-content:center;padding:12px 0}.sidebar.collapsed .sidebar-header h3{display:none}.sidebar.collapsed :is(.sidebar-header h3,.menu-item span,.submenu,.caret,.section-header){display:none}.sidebar.left{left:0}.sidebar.right{right:0}.sidebar.small{width:150px}.sidebar.medium{width:250px}.sidebar.large{width:300px}.sidebar-content-wrapper{display:flex;align-items:center;padding:16px 0;font-size:14px;color:#132644;font-weight:600}.sidebar-header{display:flex;justify-content:space-between;padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid #e0e0e0}.sidebar-header h3{margin:0;font-size:16px;color:#333;font-weight:600}.sidebar-header .toggle-btn{background:none;border:none;font-size:16px;cursor:pointer;color:#666;display:block!important}.sidebar-menu{list-style:none;padding:0;margin:0}.sidebar-menu .labels-list{margin-bottom:20px}.sidebar-menu .section-header{margin-bottom:4px;font-size:14px;color:#132644;font-weight:700;text-transform:uppercase;display:flex;align-items:center}.sidebar-menu .has-children{display:flex;flex-direction:column;gap:2px}.sidebar-menu .menu-item.active a{color:#007bff;font-weight:700}.sidebar-menu .menu-item a{display:flex;align-items:center;padding:10px;text-decoration:none;font-size:12px;font-weight:600;color:#132644;transition:background-color .2s}.sidebar-menu .menu-item a .caret{margin-right:10px;font-size:12px;font-weight:500;color:#757575;transition:transform .3s ease}.sidebar-menu .menu-item a .caret span{font-size:16px;font-weight:600}.sidebar-menu .submenu{padding:0;list-style:none;margin:0 0 16px 16px;border-left:2px solid #e0e0e0}.sidebar-menu .submenu .submenu-item{margin:0}.sidebar-menu .submenu .submenu-item.active a{color:#007bff;font-weight:700}.sidebar-menu .submenu .submenu-item a{padding:8px 0 8px 20px;display:block;color:#5e6e87;font-size:12px;font-weight:700;transition:all .3s ease}\n"] }]
}], ctorParameters: () => [{ type: SidebarService }, { type: i2.Router }, { type: i0.ElementRef }], propDecorators: { position: [{
type: Input
}], width: [{
type: Input
}], title: [{
type: Input
}], backgroundHighlightColor: [{
type: Input
}], items: [{
type: Input
}], activeItemPath: [{
type: Input
}], sidebarId: [{
type: Input
}], collapsible: [{
type: Input
}], hideSidebarOnPathChange: [{
type: Input
}], closeOnClickOutside: [{
type: Input
}], sidebarBackgroundColor: [{
type: Input
}], sidebarItemSelected: [{
type: Output
}], toggleSidebar: [{
type: Output
}], onClickOutside: [{
type: HostListener,
args: ['document:click', ['$event']]
}] } });
/*
* Public API Surface of sidebar
*/
/**
* Generated bundle index. Do not edit.
*/
export { SidebarComponent, SidebarService };
//# sourceMappingURL=ngx-smart-ngx-smart-sidebar.mjs.map