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
JavaScript
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 []="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 []="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