UNPKG

ohayolibs

Version:

Ohayo is a set of essential modules for ohayojp.

304 lines (274 loc) 9.05 kB
import { DOCUMENT } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, Input, NgZone, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation, } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { NavigationEnd, Router } from '@angular/router'; import { Menu, MenuService, SettingsService, WINDOW } from '@ohayo/theme'; import { BooleanInput, InputBoolean, InputNumber, NumberInput } from '@ohayo/util'; import { Subject } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { Nav } from './sidebar-nav.types'; const SHOWCLS = 'sidebar-nav__floating-show'; const FLOATINGCLS = 'sidebar-nav__floating'; @Component({ selector: 'sidebar-nav', exportAs: 'sidebarNav', templateUrl: './sidebar-nav.component.html', host: { '(click)': '_click()', '(document:click)': '_docClick()', }, preserveWhitespaces: false, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) export class SidebarNavComponent 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 unsubscribe$ = new Subject<void>(); private floatingEl: HTMLDivElement; list: Nav[] = []; @Input() @InputBoolean() disabledAcl = false; @Input() @InputBoolean() autoCloseUnderPad = true; @Input() @InputBoolean() recursivePath = true; @Input() @InputBoolean() openStrictly = false; @Input() @InputNumber() maxLevelIcon = 3; // tslint:disable-next-line:no-output-native @Output() readonly select = new EventEmitter<Menu>(); get collapsed(): boolean { return this.settings.layout.collapsed; } constructor( private menuSrv: MenuService, private settings: SettingsService, private router: Router, private render: Renderer2, private cdr: ChangeDetectorRef, private ngZone: NgZone, private sanitizer: DomSanitizer, @Inject(DOCUMENT) private doc: any, @Inject(WINDOW) private win: Window, ) { } private getLinkNode(node: HTMLElement): HTMLElement | null { node = node.nodeName === 'A' ? node : (node.parentNode as HTMLElement); return node.nodeName !== 'A' ? null : node; } private floatingClickHandle(e: MouseEvent): boolean { e.stopPropagation(); const linkNode = this.getLinkNode(e.target as HTMLElement); if (linkNode == null) { return false; } const id = +linkNode.dataset!.id!; // Should be ingore children title trigger event if (isNaN(id)) { return false; } let item: Nav; this.menuSrv.visit(this.list, (i: Nav) => { if (!item && i._id === id) { item = i; } }); this.to(item!); this.hideAll(); e.preventDefault(); return false; } private clearFloating(): void { if (!this.floatingEl) return; this.floatingEl.removeEventListener('click', this.floatingClickHandle.bind(this)); // fix ie: https://github.com/ohayojp/ohayo/issues/52 if (this.floatingEl.hasOwnProperty('remove')) { this.floatingEl.remove(); } else if (this.floatingEl.parentNode) { this.floatingEl.parentNode.removeChild(this.floatingEl); } } private genFloating(): void { 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: HTMLLinkElement, item: Nav): HTMLUListElement { 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(): void { const allNode = this.floatingEl.querySelectorAll('.' + FLOATINGCLS); // tslint:disable-next-line:prefer-for-of for (let i = 0; i < allNode.length; i++) { allNode[i].classList.remove(SHOWCLS); } } // calculate the node position values. private calPos(linkNode: HTMLLinkElement, node: HTMLUListElement): void { const rect = linkNode.getBoundingClientRect(); // bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14721015/ 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`; node.style.left = `${rect.right + spacing}px`; } showSubMenu(e: MouseEvent, item: Nav): void { if (this.collapsed !== true) { return; } this.ngZone.runOutsideAngular(() => { 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: Menu): void { 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: Nav): void { if (!this.openStrictly) { this.menuSrv.visit(this.list, (i: Nav) => { 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(): void { if (this.isPad && this.collapsed) { this.openAside(false); this.hideAll(); } } _docClick(): void { if (this.collapsed) { this.hideAll(); } } private openedByUrl(url: string | null): void { const { menuSrv, recursivePath, openStrictly } = this; let findItem: Nav | null = menuSrv.getHit(this.menuSrv.menus, url!, recursivePath, (i: Nav) => { 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(): void { const { doc, router, unsubscribe$, menuSrv, settings, cdr } = this; this.bodyEl = doc.querySelector('body'); this.openedByUrl(router.url); this.ngZone.runOutsideAngular(() => this.genFloating()); menuSrv.change.pipe(takeUntil(unsubscribe$)).subscribe(data => { menuSrv.visit(data, (i: Nav, _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; } }); this.list = menuSrv.menus.filter((w: Nav) => w._hidden !== true); cdr.detectChanges(); }); router.events.pipe(takeUntil(unsubscribe$)).subscribe(e => { if (e instanceof NavigationEnd) { this.openedByUrl(e.urlAfterRedirects); this.underPad(); this.cdr.detectChanges(); } }); settings.notify .pipe( takeUntil(unsubscribe$), filter(t => t.type === 'layout' && t.name === 'collapsed'), ) .subscribe(() => this.clearFloating()); this.underPad(); } ngOnDestroy(): void { const { unsubscribe$ } = this; unsubscribe$.next(); unsubscribe$.complete(); this.clearFloating(); } // #region Under pad private get isPad(): boolean { return window.innerWidth < 768; } private underPad(): void { if (this.autoCloseUnderPad && this.isPad && !this.collapsed) { setTimeout(() => this.openAside(true)); } } private openAside(status: boolean): void { this.settings.setLayout('collapsed', status); } // #endregion }