UNPKG

angular-sidebar-menu

Version:

The sources for this package are in the [Angular Sidebar Menu](https://github.com/mledour/angular-sidebar-menu) repo. Please file issues and pull requests against that repo.

484 lines (467 loc) 20.5 kB
import { Injectable, Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, HostBinding, EventEmitter, Output, ViewChildren, ViewChild, NgModule } from '@angular/core'; import { Subject, BehaviorSubject, combineLatest } from 'rxjs'; import { map, distinctUntilChanged, takeUntil, filter } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { trigger, state, style, AUTO_STYLE, transition, animate } from '@angular/animations'; var Modes; (function (Modes) { Modes["EXPANDED"] = "expanded"; Modes["EXPANDABLE"] = "expandable"; Modes["MINI"] = "mini"; })(Modes || (Modes = {})); class AnchorService { } AnchorService.decorators = [ { type: Injectable } ]; class NodeService { constructor() { this.openedNode = new Subject(); } } NodeService.decorators = [ { type: Injectable } ]; class RoleService { constructor() { this.role$ = new BehaviorSubject(undefined); this.unAuthorizedVisibility$ = new BehaviorSubject('hidden'); } set role(role) { this.role$.next(role); } set unAuthorizedVisibility(visibility) { this.unAuthorizedVisibility$.next(visibility); } showItem$(roles) { return this.itemVisibilityBase$(roles).pipe(map((values) => values.isAuthorized || (!values.isAuthorized && values.unAuthorizedVisibility !== 'hidden'))); } disableItem$(roles) { return this.itemVisibilityBase$(roles).pipe(map((values) => !values.isAuthorized && values.unAuthorizedVisibility === 'disabled')); } itemVisibilityBase$(roles) { return combineLatest([ this.role$.pipe(map((role) => this.isAuthorized(role, roles))), this.unAuthorizedVisibility$, ]).pipe(map((value) => ({ isAuthorized: value[0], unAuthorizedVisibility: value[1] }))); } isRole(role) { return typeof role === 'string' || typeof role === 'number'; } isAuthorized(userRole, itemRoles) { if (!this.isRole(userRole) || !itemRoles || itemRoles.length === 0) { return true; } return itemRoles.includes(userRole); } } RoleService.decorators = [ { type: Injectable } ]; class SearchService { constructor() { this._search = new Subject(); this.search$ = this._search.asObservable(); } set search(value) { this._search.next(value); } filter(search, label) { if (!search || !label) { return false; } return !label.toLowerCase().includes(search.toLowerCase()); } } SearchService.decorators = [ { type: Injectable } ]; const trackByItem = (index, item) => { return item.id || index; }; class SidebarMenuComponent { constructor(anchorService, nodeService, searchService, roleService) { this.anchorService = anchorService; this.nodeService = nodeService; this.searchService = searchService; this.roleService = roleService; this.mode = Modes.EXPANDED; this.modes = Modes; this.disableAnimations = true; this.trackByItem = trackByItem; } set _menu(menu) { this.disableAnimations = true; this.menu = menu; setTimeout(() => { this.disableAnimations = false; }); } set iconClasses(cssClasses) { this.anchorService.iconClasses = cssClasses; } set toggleIconClasses(cssClasses) { this.nodeService.toggleIconClasses = cssClasses; } set role(role) { this.roleService.role = role; } set unAuthorizedVisibility(visibility) { this.roleService.unAuthorizedVisibility = visibility; } set search(value) { this.searchService.search = value; } } SidebarMenuComponent.decorators = [ { type: Component, args: [{ selector: 'asm-angular-sidebar-menu', providers: [NodeService, AnchorService, RoleService, SearchService], changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div class="asm-menu" [ngClass]="'asm-menu--mode-' + mode" [@.disabled]="disableAnimations"> <ng-content></ng-content> <ul class="asm-menu__node"> <ng-container *ngFor="let item of menu; trackBy: trackByItem"> <li asm-menu-item class="asm-menu-item asm-menu-item--root" *ngIf="roleService.showItem$(item.roles) | async" [menuItem]="item" [level]="0" ></li> </ng-container> </ul> </div>`, styles: [":host{display:block;overflow:hidden}:host:hover{overflow:visible}:host ::ng-deep .asm-menu-item,:host ::ng-deep ul{margin:0;padding:0}:host ::ng-deep li{line-height:0}:host ::ng-deep .asm-menu-anchor__label,:host ::ng-deep .asm-menu-item__header,:host ::ng-deep .asm-menu-node__label{white-space:nowrap}:host ::ng-deep .asm-menu-node__label{display:none}:host ::ng-deep .asm-menu-node:not(.asm-menu-node--open)>.ng-trigger-openClose,:host ::ng-deep .asm-menu-node>.ng-trigger-openClose.ng-animating{overflow:hidden}:host ::ng-deep .asm-menu-item--filtered{display:none}:host ::ng-deep .asm-menu,:host ::ng-deep .asm-menu-node ul{list-style:none}:host ::ng-deep .asm-menu-anchor a{-webkit-user-select:none;align-items:center;cursor:pointer;display:flex;position:relative;text-decoration:none;user-select:none}:host ::ng-deep .asm-menu-item__header{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .asm-menu-node .asm-menu-item__header{margin-left:-6px}:host ::ng-deep .asm-menu-anchor__icon{width:20px}:host ::ng-deep .asm-menu-anchor__pull.asm-badges,:host ::ng-deep .asm-menu-anchor__pull.asm-toggle{position:absolute;right:10px}:host ::ng-deep .asm-menu-anchor__pull.asm-badges .asm-badges__badge,:host ::ng-deep .asm-menu-anchor__pull.asm-badges .asm-toggle__icon,:host ::ng-deep .asm-menu-anchor__pull.asm-toggle .asm-badges__badge,:host ::ng-deep .asm-menu-anchor__pull.asm-toggle .asm-toggle__icon{float:right;margin-left:4px;text-align:center;white-space:nowrap}:host ::ng-deep .asm-menu-anchor__pull.asm-badges .asm-toggle__icon,:host ::ng-deep .asm-menu-anchor__pull.asm-toggle .asm-toggle__icon{margin:0 4px 0 8px}:host ::ng-deep .asm-menu--mode-expandable:not(:hover),:host ::ng-deep .asm-menu--mode-mini{width:50px}:host ::ng-deep .asm-menu--mode-expandable:not(:hover)>ul>.asm-menu-item>.asm-menu-anchor .asm-menu-anchor__label,:host ::ng-deep .asm-menu--mode-expandable:not(:hover)>ul>.asm-menu-item>.asm-menu-anchor .asm-menu-anchor__pull,:host ::ng-deep .asm-menu--mode-expandable:not(:hover)>ul>.asm-menu-item>.asm-menu-item__header,:host ::ng-deep .asm-menu--mode-expandable:not(:hover)>ul>.asm-menu-item>.asm-menu-node>.asm-menu-anchor .asm-menu-anchor__label,:host ::ng-deep .asm-menu--mode-expandable:not(:hover)>ul>.asm-menu-item>.asm-menu-node>.asm-menu-anchor .asm-menu-anchor__pull,:host ::ng-deep .asm-menu--mode-expandable:not(:hover)>ul>.asm-menu-item>.asm-menu-node>ul,:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item>.asm-menu-anchor .asm-menu-anchor__label,:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item>.asm-menu-anchor .asm-menu-anchor__pull,:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item>.asm-menu-item__header,:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item>.asm-menu-node>.asm-menu-anchor .asm-menu-anchor__label,:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item>.asm-menu-node>.asm-menu-anchor .asm-menu-anchor__pull,:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item>.asm-menu-node>ul{display:none}:host ::ng-deep .asm-menu--mode-mini .asm-menu-node__label{display:block}:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item{position:relative}:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item:hover>.asm-menu-anchor .asm-menu-anchor__label,:host ::ng-deep .asm-menu--mode-mini>ul>.asm-menu-item:hover>.asm-menu-node>ul{display:block!important;height:auto!important;left:100%;position:absolute;top:0;z-index:999}"] },] } ]; SidebarMenuComponent.ctorParameters = () => [ { type: AnchorService }, { type: NodeService }, { type: SearchService }, { type: RoleService } ]; SidebarMenuComponent.propDecorators = { _menu: [{ type: Input, args: ['menu',] }], iconClasses: [{ type: Input }], toggleIconClasses: [{ type: Input }], role: [{ type: Input }], unAuthorizedVisibility: [{ type: Input }], search: [{ type: Input }], mode: [{ type: Input }] }; const TRANSITION_DURATION = 300; const openCloseAnimation = trigger('openClose', [ state('true', style({ height: AUTO_STYLE })), state('false', style({ height: 0 })), transition('false <=> true', animate(`${TRANSITION_DURATION}ms ease-in`)), ]); const rotateAnimation = trigger('rotate', [ state('true', style({ transform: 'rotate(-90deg)' })), transition('false <=> true', animate(`${TRANSITION_DURATION}ms ease-out`)), ]); class ItemComponent { constructor(router, roleService, searchService, changeDetectorRef) { this.router = router; this.roleService = roleService; this.searchService = searchService; this.changeDetectorRef = changeDetectorRef; this.isRootNode = true; this.disable = false; this.onDestroy$ = new Subject(); this.isActive = new BehaviorSubject(false); this.isFiltered = new BehaviorSubject(false); this.isActive$ = this.isActive.asObservable().pipe(distinctUntilChanged(), takeUntil(this.onDestroy$)); this.isFiltered$ = this.isFiltered.asObservable().pipe(distinctUntilChanged(), takeUntil(this.onDestroy$)); this.isItemFiltered = false; this.isItemDisabled = false; } get filtered() { return this.isItemFiltered; } get disabled() { return this.isItemDisabled || this.disable; } ngOnInit() { this.routerItemActiveSubscription(); this.emitItemActive(); this.menuSearchSubscription(); this.disabledItemSubscription(); } ngOnDestroy() { this.onDestroy$.next(); this.onDestroy$.complete(); } onNodeActive(event) { this.isActive.next(event); } onNodeFiltered(event) { this.isItemFiltered = event; this.isFiltered.next(event); } routerItemActiveSubscription() { this.router.events .pipe(filter((e) => e instanceof NavigationEnd), takeUntil(this.onDestroy$)) .subscribe((e) => { this.emitItemActive(); }); } menuSearchSubscription() { if (!this.menuItem.children) { this.searchService.search$.pipe(takeUntil(this.onDestroy$)).subscribe((search) => { this.isItemFiltered = this.searchService.filter(search, this.menuItem.label || this.menuItem.header); this.isFiltered.next(this.isItemFiltered); this.changeDetectorRef.markForCheck(); }); } } disabledItemSubscription() { this.roleService .disableItem$(this.menuItem.roles) .pipe(takeUntil(this.onDestroy$)) .subscribe((disabled) => (this.isItemDisabled = disabled)); } emitItemActive() { if (this.menuItem.route) { this.isActive.next(this.isActiveRoute(this.menuItem.route)); } } isActiveRoute(route) { return this.router.isActive(route, this.isItemLinkExact()); } isItemLinkExact() { return this.menuItem.linkActiveExact === undefined ? true : this.menuItem.linkActiveExact; } } ItemComponent.decorators = [ { type: Component, args: [{ // tslint:disable-next-line:component-selector selector: 'li[asm-menu-item][menuItem]', animations: [rotateAnimation], changeDetection: ChangeDetectionStrategy.OnPush, template: ` <ng-container [ngSwitch]="true"> <span *ngSwitchCase="!!menuItem.header" class="asm-menu-item__header">{{ menuItem.header }}</span> <asm-menu-anchor *ngSwitchCase="!menuItem.children && !menuItem.header" class="asm-menu-anchor" [menuItem]="menuItem" [disable]="disable || isItemDisabled" ></asm-menu-anchor> <ng-container *ngSwitchCase="!!menuItem.children"> <asm-menu-anchor class="asm-menu-anchor" [ngClass]="{ 'asm-menu-anchor--open': node.isOpen }" [menuItem]="menuItem" (clickAnchor)="node.onNodeToggleClick()" [isActive]="node.isActiveChild" ><i toggleIcon [@rotate]="node.isOpen" [class]="node.nodeService.toggleIconClasses"></i ></asm-menu-anchor> <asm-menu-node #node class="asm-menu-node" [menuItem]="menuItem" [level]="level" [disable]="disable || isItemDisabled" (isActive)="onNodeActive($event)" (isFiltered)="onNodeFiltered($event)" ></asm-menu-node> </ng-container> </ng-container> ` },] } ]; ItemComponent.ctorParameters = () => [ { type: Router }, { type: RoleService }, { type: SearchService }, { type: ChangeDetectorRef } ]; ItemComponent.propDecorators = { menuItem: [{ type: Input }], isRootNode: [{ type: Input }], level: [{ type: Input }], disable: [{ type: Input }], filtered: [{ type: HostBinding, args: ['class.asm-menu-item--filtered',] }], disabled: [{ type: HostBinding, args: ['class.asm-menu-item--disabled',] }] }; class NodeComponent { constructor(nodeService, roleService, changeDetectorRef) { this.nodeService = nodeService; this.roleService = roleService; this.changeDetectorRef = changeDetectorRef; this.disable = false; this.isActive = new EventEmitter(); this.isFiltered = new EventEmitter(); this.isOpen = false; this.isActiveChild = false; this.trackByItem = trackByItem; this.onDestroy$ = new Subject(); } get open() { return this.isOpen; } ngAfterViewInit() { this.openedNodeSubscription(); this.activeItemsSubscription(); this.filterItemsSubscription(); } ngOnDestroy() { this.onDestroy$.next(); this.onDestroy$.complete(); } onNodeToggleClick() { this.isOpen = !this.isOpen; this.nodeService.openedNode.next({ nodeComponent: this, nodeLevel: this.level }); this.changeDetectorRef.markForCheck(); } activeItemsSubscription() { const isChildrenItemsActive = this.menuItemComponents.map((item) => item.isActive$); if (isChildrenItemsActive && isChildrenItemsActive.length) { combineLatest(isChildrenItemsActive) .pipe(takeUntil(this.onDestroy$)) .subscribe((itemsActiveState) => { this.isOpen = this.isActiveChild = itemsActiveState.includes(true); this.isActive.emit(this.isOpen); }); } } filterItemsSubscription() { const isChildrenItemsFiltered = this.menuItemComponents.map((item) => item.isFiltered$); if (isChildrenItemsFiltered && isChildrenItemsFiltered.length) { combineLatest(isChildrenItemsFiltered) .pipe(takeUntil(this.onDestroy$)) .subscribe((itemsFilteredState) => { const isItemsFiltered = itemsFilteredState.includes(false) === false; this.isFiltered.emit(isItemsFiltered); }); } } openedNodeSubscription() { this.nodeService.openedNode .pipe(filter(() => !!this.isOpen), filter((node) => node.nodeComponent !== this), takeUntil(this.onDestroy$)) .subscribe((node) => { if (node.nodeLevel <= this.level) { this.isOpen = false; this.changeDetectorRef.markForCheck(); } }); } } NodeComponent.decorators = [ { type: Component, args: [{ selector: 'asm-menu-node', animations: [openCloseAnimation], changeDetection: ChangeDetectionStrategy.OnPush, template: `<ul [@openClose]="isOpen"> <li *ngIf="level === 0" class="asm-menu-item"> <span class="asm-menu-node__label">{{ menuItem.label }}</span> </li> <ng-container *ngFor="let childItem of menuItem.children; trackBy: trackByItem"> <li asm-menu-item class="asm-menu-item" *ngIf="roleService.showItem$(childItem.roles) | async" [menuItem]="childItem" [level]="level + 1" [disable]="disable" ></li> </ng-container> </ul>` },] } ]; NodeComponent.ctorParameters = () => [ { type: NodeService }, { type: RoleService }, { type: ChangeDetectorRef } ]; NodeComponent.propDecorators = { menuItem: [{ type: Input }], level: [{ type: Input }], disable: [{ type: Input }], isActive: [{ type: Output }], isFiltered: [{ type: Output }], open: [{ type: HostBinding, args: ['class.asm-menu-node--open',] }], menuItemComponents: [{ type: ViewChildren, args: [ItemComponent,] }] }; class AnchorComponent { constructor(anchorService) { this.anchorService = anchorService; this.disable = false; this.clickAnchor = new EventEmitter(); } get active() { var _a; return this.isActive || (!!((_a = this.routerLinActive) === null || _a === void 0 ? void 0 : _a.isActive) && !this.disable); } } AnchorComponent.decorators = [ { type: Component, args: [{ selector: 'asm-menu-anchor', changeDetection: ChangeDetectionStrategy.OnPush, template: `<ng-container [ngSwitch]="true"> <a *ngSwitchCase="!!menuItem.children" (click)="clickAnchor.emit()"> <ng-container *ngTemplateOutlet="innerItem"></ng-container> </a> <a *ngSwitchCase="!!menuItem.route || menuItem.route === ''" [routerLink]="disable ? undefined : menuItem.route" routerLinkActive #rla="routerLinkActive" [routerLinkActiveOptions]="{ exact: menuItem.linkActiveExact === undefined ? true : menuItem.linkActiveExact }" > <ng-container *ngTemplateOutlet="innerItem"></ng-container> </a> <a *ngSwitchCase="!!menuItem.url" [href]="menuItem.url" [target]="menuItem.target"> <ng-container *ngTemplateOutlet="innerItem"></ng-container> </a> </ng-container> <ng-template #innerItem> <i *ngIf="menuItem.iconClasses || anchorService.iconClasses" [class]="menuItem.iconClasses || anchorService.iconClasses" class="asm-menu-anchor__icon" ></i> <span class="asm-menu-anchor__label">{{ menuItem.label }}</span> <span *ngIf="menuItem.badges || menuItem.children" class="asm-menu-anchor__pull" [ngClass]="{ 'asm-badges': menuItem.badges, 'asm-toggle': menuItem.children }" > <span *ngFor="let badge of menuItem.badges" [class]="badge.classes" class="asm-badges__badge">{{ badge.label }}</span> <span class="asm-toggle__icon"><ng-content select="[toggleIcon]"></ng-content></span> </span> </ng-template>` },] } ]; AnchorComponent.ctorParameters = () => [ { type: AnchorService } ]; AnchorComponent.propDecorators = { menuItem: [{ type: Input }], isActive: [{ type: Input }], disable: [{ type: Input }], clickAnchor: [{ type: Output }], active: [{ type: HostBinding, args: ['class.asm-menu-anchor--active',] }], routerLinActive: [{ type: ViewChild, args: ['rla',] }] }; class SidebarMenuModule { } SidebarMenuModule.decorators = [ { type: NgModule, args: [{ declarations: [SidebarMenuComponent, ItemComponent, NodeComponent, AnchorComponent], imports: [RouterModule, CommonModule], exports: [SidebarMenuComponent], },] } ]; /* * Public API Surface of angular-sidebar-menu */ /** * Generated bundle index. Do not edit. */ export { Modes, SidebarMenuComponent, SidebarMenuModule, NodeService as ɵa, AnchorService as ɵb, RoleService as ɵc, SearchService as ɵd, ItemComponent as ɵe, openCloseAnimation as ɵf, rotateAnimation as ɵg, NodeComponent as ɵh, AnchorComponent as ɵi }; //# sourceMappingURL=angular-sidebar-menu.js.map