ohayolibs
Version:
Ohayo is a set of essential modules for ohayojp.
304 lines (274 loc) • 9.05 kB
text/typescript
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';
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[] = [];
disabledAcl = false;
autoCloseUnderPad = true;
recursivePath = true;
openStrictly = false;
maxLevelIcon = 3;
// tslint:disable-next-line:no-output-native
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,
private doc: any,
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
}