import { Direction , Directionality } from '@angular/cdk/bidi' ; import { DOCUMENT } from '@angular/common' ; import { ChangeDetectionStrategy , ChangeDetectorRef , Component , EventEmitter , Inject , Input , NgZone , OnDestroy , OnInit , Optional , Output , Renderer2 , ViewEncapsulation } from '@angular/core' ; import { DomSanitizer , SafeHtml } from '@angular/platform-browser' ; import { NavigationEnd , Router } from '@angular/router' ; import { Subject } from 'rxjs' ; import { filter , takeUntil } from 'rxjs/operators' ; import { Menu , MenuIcon , MenuInner , MenuService , SettingsService } from '@delon/theme' ; import { BooleanInput , InputBoolean , InputNumber , NumberInput , ZoneOutside } from '@delon/util/decorator' ; import { WINDOW } from '@delon/util/token' ; import type { NzSafeAny } from 'ng-zorro-antd/core/types' ; export interface Nav extends MenuInner { _needIcon ? : boolean ; _text ? : SafeHtml ; } const SHOWCLS = 'sidebar-nav__floating-show' ; const FLOATINGCLS = 'sidebar-nav__floating' ; @ Component ( { selector : 'layout-default-nav' , templateUrl : './layout-nav.component.html' , host : { '(click)' : '_click()' , '(document:click)' : 'closeSubMenu()' } , preserveWhitespaces : false , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None } ) export class LayoutDefaultNavComponent implements OnInit , OnDestroy { static ngAcceptInputType_disabledAcl : BooleanInput ; static ngAcceptInputType_autoCloseUnderPad : BooleanInput ; static ngAcceptInputType_recursivePath : BooleanInput ; static ngAcceptInputType_openStrictly : BooleanInput ; static ngAcceptInputType_maxLevelIcon : NumberInput ; private bodyEl ! : HTMLBodyElement ; private destroy$ = new Subject < void > ( ) ; private floatingEl ! : HTMLDivElement ; dir : Direction = 'ltr' ; list : Nav [ ] = [ ] ; @ Input ( ) @ InputBoolean ( ) disabledAcl = false ; @ Input ( ) @ InputBoolean ( ) autoCloseUnderPad = true ; @ Input ( ) @ InputBoolean ( ) recursivePath = true ; @ Input ( ) @ InputBoolean ( ) openStrictly = false ; @ Input ( ) @ InputNumber ( ) maxLevelIcon = 3 ; @ Output ( ) readonly select = new EventEmitter < Menu > ( ) ; get collapsed ( ) : boolean { return this . settings . layout . collapsed ; } constructor ( private menuSrv , private settings , private router , private render , private cdr , private ngZone , private sanitizer , @ Inject ( DOCUMENT ) private doc , @ Inject ( WINDOW ) private win , @ Optional ( ) private directionality ) { } private getLinkNode ( node ) { node = node . nodeName === 'A' ? node : ( node . parentNode as HTMLElement ) ; return node . nodeName !== 'A' ? null : node ; } private floatingClickHandle ( e ) { e . stopPropagation ( ) ; const linkNode = this . getLinkNode ( e . target as HTMLElement ) ; if ( linkNode == null ) { return false ; } const id = + linkNode . dataset ! . id ! ; if ( isNaN ( id ) ) { return false ; } let item ; this . menuSrv . visit ( this . list , ( i ) => { if ( ! item && i . _id === id ) { item = i ; } } ) ; this . to ( item ! ) ; this . hideAll ( ) ; e . preventDefault ( ) ; return false ; } private clearFloating ( ) { if ( ! this . floatingEl ) return ; this . floatingEl . removeEventListener ( 'click' , this . floatingClickHandle . bind ( this ) ) ; if ( this . floatingEl . hasOwnProperty ( 'remove' ) ) { this . floatingEl . remove ( ) ; } else if ( this . floatingEl . parentNode ) { this . floatingEl . parentNode . removeChild ( this . floatingEl ) ; } } private genFloating ( ) { this . clearFloating ( ) ; this . floatingEl = this . render . createElement ( 'div' ) ; this . floatingEl . classList . add ( `${ FLOATINGCLS }-container` ) ; this . floatingEl . addEventListener ( 'click' , this . floatingClickHandle . bind ( this ) , false ) ; this . bodyEl . appendChild ( this . floatingEl ) ; } private genSubNode ( linkNode , item ) { const id = `_sidebar-nav-${ item . _id }` ; const childNode = item . badge ? linkNode . nextElementSibling ! . nextElementSibling ! : linkNode . nextElementSibling ! ; const node = childNode . cloneNode ( true ) as HTMLUListElement ; node . id = id ; node . classList . add ( FLOATINGCLS ) ; node . addEventListener ( 'mouseleave' , ( ) => { node . classList . remove ( SHOWCLS ) ; } , false ) ; this . floatingEl . appendChild ( node ) ; return node ; } private hideAll ( ) { const allNode = this . floatingEl . querySelectorAll ( `.${ FLOATINGCLS }` ) ; for ( let i = 0 ; i < allNode . length ; i ++ ) { allNode [ i ] . classList . remove ( SHOWCLS ) ; } } private calPos ( linkNode , node ) { const rect = linkNode . getBoundingClientRect ( ) ; const scrollTop = Math . max ( this . doc . documentElement . scrollTop , this . bodyEl . scrollTop ) ; const docHeight = Math . max ( this . doc . documentElement . clientHeight , this . bodyEl . clientHeight ) ; const spacing = 5 ; let offsetHeight = - spacing ; if ( docHeight < rect . top + node . clientHeight ) { offsetHeight = rect . top + node . clientHeight - docHeight + spacing ; } node . style . top = `${ rect . top + scrollTop - offsetHeight }px` ; if ( this . dir === 'rtl' ) { node . style . right = `${ rect . width + spacing }px` ; } else { node . style . left = `${ rect . right + spacing }px` ; } } @ ZoneOutside ( ) showSubMenu ( e , item ) { if ( this . collapsed !== true ) { return ; } e . preventDefault ( ) ; const linkNode = e . target as Element ; this . genFloating ( ) ; const subNode = this . genSubNode ( linkNode as HTMLLinkElement , item ) ; this . hideAll ( ) ; subNode . classList . add ( SHOWCLS ) ; this . calPos ( linkNode as HTMLLinkElement , subNode ) ; } to ( item ) { this . select . emit ( item ) ; if ( item . disabled ) return ; if ( item . externalLink ) { if ( item . target === '_blank' ) { this . win . open ( item . externalLink ) ; } else { this . win . location . href = item . externalLink ; } return ; } this . ngZone . run ( ( ) => this . router . navigateByUrl ( item . link ! ) ) ; } toggleOpen ( item ) { if ( ! this . openStrictly ) { this . menuSrv . visit ( this . list , ( i ) => { if ( i !== item ) i . _open = false ; } ) ; let pItem = item . _parent as Nav ; while ( pItem ) { pItem . _open = true ; pItem = pItem . _parent ! ; } } item . _open = ! item . _open ; this . cdr . markForCheck ( ) ; } _click ( ) { if ( this . isPad && this . collapsed ) { this . openAside ( false ) ; this . hideAll ( ) ; } } closeSubMenu ( ) { if ( this . collapsed ) { this . hideAll ( ) ; } } private openedByUrl ( url ) { const { menuSrv , recursivePath , openStrictly } = this ; let findItem = menuSrv . getHit ( this . menuSrv . menus , url ! , recursivePath , ( i ) => { i . _selected = false ; if ( ! openStrictly ) { i . _open = false ; } } ) ; if ( findItem == null ) return ; do { findItem . _selected = true ; if ( ! openStrictly ) { findItem . _open = true ; } findItem = findItem . _parent ! ; } while ( findItem ) ; } ngOnInit ( ) { const { doc , router , destroy$ , menuSrv , settings , cdr } = this ; this . bodyEl = doc . querySelector ( 'body' ) ; this . openedByUrl ( router . url ) ; this . ngZone . runOutsideAngular ( ( ) => this . genFloating ( ) ) ; menuSrv . change . pipe ( takeUntil ( destroy$ ) ) . subscribe ( data => { menuSrv . visit ( data , ( i , _p , depth ) => { i . _text = this . sanitizer . bypassSecurityTrustHtml ( i . text ! ) ; i . _needIcon = depth ! <= this . maxLevelIcon && ! ! i . icon ; if ( ! i . _aclResult ) { if ( this . disabledAcl ) { i . disabled = true ; } else { i . _hidden = true ; } } if ( this . openStrictly ) { i . _open = i . open != null ? i . open : false ; } const icon = i . icon as MenuIcon ; if ( icon && icon . type === 'svg' && typeof icon . value === 'string' ) { icon . value = this . sanitizer . bypassSecurityTrustHtml ( icon . value ! ! ) ; } } ) ; this . list = menuSrv . menus . filter ( ( w ) => w . _hidden !== true ) ; cdr . detectChanges ( ) ; } ) ; router . events . pipe ( takeUntil ( destroy$ ) ) . subscribe ( e => { if ( e instanceof NavigationEnd ) { this . openedByUrl ( e . urlAfterRedirects ) ; this . underPad ( ) ; this . cdr . detectChanges ( ) ; } } ) ; settings . notify . pipe ( takeUntil ( destroy$ ) , filter ( t => t . type === 'layout' && t . name === 'collapsed' ) ) . subscribe ( ( ) => this . clearFloating ( ) ) ; this . underPad ( ) ; this . dir = this . directionality . value ; this . directionality . change ?. pipe ( takeUntil ( destroy$ ) ) . subscribe ( ( direction ) => { this . dir = direction ; } ) ; } ngOnDestroy ( ) { this . destroy$ . next ( ) ; this . destroy$ . complete ( ) ; this . clearFloating ( ) ; } private get isPad ( ) : boolean { return this . doc . defaultView ! . innerWidth < 768 ; } private underPad ( ) { if ( this . autoCloseUnderPad && this . isPad && ! this . collapsed ) { setTimeout ( ( ) => this . openAside ( true ) ) ; } } private openAside ( status ) { this . settings . setLayout ( 'collapsed' , status ) ; } }