UNPKG

@ngx-smart/ngx-smart-sidebar

Version:

A customizable, collapsible, responsive, feature-rich sidebar component for Angular apps.

380 lines (370 loc) 18.8 kB
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