projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.ts
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | cx-navigation-ui |
| templateUrl | ./navigation-ui.component.html |
Properties |
|
Methods |
Inputs |
HostListeners |
constructor(router: Router, renderer: Renderer2, elemRef: ElementRef, hamburgerMenuService: HamburgerMenuService)
|
|||||||||||||||
|
Parameters :
|
| flyout | |
Type : boolean
|
|
Default value : true
|
|
|
Indicates whether the navigation should support flyout. If flyout is set to true, the nested child navigation nodes will only appear on hover or focus. |
|
| isOpen | |
Type : boolean
|
|
Default value : false
|
|
| node | |
Type : NavigationNode
|
|
|
The navigation node to render. |
|
| resetMenuOnClose | |
Type : boolean
|
|
|
Flag indicates whether to reset the state of menu navigation (ie. Collapse all submenus) when the menu is closed. |
|
| wrapAfter | |
Type : number
|
|
|
The number of child nodes that must be wrapped. |
|
| window:resize |
window:resize()
|
| Private alignWrappersToRightIfStickOut |
alignWrappersToRightIfStickOut()
|
|
Returns :
void
|
| Private alignWrapperToRightIfStickOut | ||||||
alignWrapperToRightIfStickOut(node: HTMLElement)
|
||||||
|
Parameters :
Returns :
void
|
| back |
back()
|
|
Returns :
void
|
| clear |
clear()
|
|
Returns :
void
|
| focusAfterPreviousClicked | ||||||
focusAfterPreviousClicked(event: MouseEvent)
|
||||||
|
Parameters :
Returns :
any
|
| getColumnCount | ||||||
getColumnCount(length: number)
|
||||||
|
Parameters :
Returns :
number
|
| getTotalDepth | ||||||||||||
getTotalDepth(node: NavigationNode, depth: number)
|
||||||||||||
|
Parameters :
Returns :
number
|
| ngOnDestroy |
ngOnDestroy()
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
During initialization of this component, we will check the resetMenuOnClose flag and attach a menu reset listener if needed.
Returns :
void
|
| onMouseEnter | ||||||
onMouseEnter(event: MouseEvent)
|
||||||
|
Parameters :
Returns :
void
|
| onResize |
onResize()
|
Decorators :
@HostListener('window:resize')
|
|
Returns :
void
|
| reinitalizeMenu |
reinitalizeMenu()
|
|
This method performs the actions required to reset the state of the menu and reset any visual components.
Returns :
void
|
| resetOnMenuCollapse |
resetOnMenuCollapse()
|
|
This method performs the action of resetting the menu (close all sub menus and return to main options) when the menu is closed.
Returns :
void
|
| toggleOpen | ||||||
toggleOpen(event: UIEvent)
|
||||||
|
Parameters :
Returns :
void
|
| Private updateClasses |
updateClasses()
|
|
Returns :
void
|
| iconType |
Default value : ICON_TYPE
|
|
the icon type that will be used for navigation nodes with children. |
| isOpen |
Default value : false
|
Decorators :
@Input()
|
| node |
Type : NavigationNode
|
Decorators :
@Input()
|
|
The navigation node to render. |
| Private openNodes |
Type : HTMLElement[]
|
Default value : []
|
| resetMenuOnClose |
Type : boolean
|
Decorators :
@Input()
|
|
Flag indicates whether to reset the state of menu navigation (ie. Collapse all submenus) when the menu is closed. |
| Private resize |
Default value : new EventEmitter()
|
| Private subscriptions |
Default value : new Subscription()
|
| wrapAfter |
Type : number
|
Decorators :
@Input()
|
|
The number of child nodes that must be wrapped. |
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
HostBinding,
HostListener,
Input,
OnDestroy,
Renderer2,
OnInit,
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { debounceTime, filter } from 'rxjs/operators';
import { ICON_TYPE } from '../../misc/icon/index';
import { NavigationNode } from './navigation-node.model';
import { distinctUntilChanged } from 'rxjs/operators';
import { HamburgerMenuService } from './../../../layout/header/hamburger-menu/hamburger-menu.service';
@Component({
selector: 'cx-navigation-ui',
templateUrl: './navigation-ui.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NavigationUIComponent implements OnInit, OnDestroy {
/**
* The navigation node to render.
*/
@Input() node: NavigationNode;
/**
* The number of child nodes that must be wrapped.
*/
@Input() wrapAfter: number;
/**
* Flag indicates whether to reset the state of menu navigation (ie. Collapse all submenus) when the menu is closed.
*/
@Input() resetMenuOnClose: boolean;
/**
* the icon type that will be used for navigation nodes
* with children.
*/
iconType = ICON_TYPE;
/**
* Indicates whether the navigation should support flyout.
* If flyout is set to true, the
* nested child navigation nodes will only appear on hover or focus.
*/
@Input() @HostBinding('class.flyout') flyout = true;
@Input() @HostBinding('class.is-open') isOpen = false;
private openNodes: HTMLElement[] = [];
private subscriptions = new Subscription();
private resize = new EventEmitter();
@HostListener('window:resize')
onResize() {
this.resize.next();
}
constructor(
private router: Router,
private renderer: Renderer2,
private elemRef: ElementRef,
protected hamburgerMenuService: HamburgerMenuService
) {
this.subscriptions.add(
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
.subscribe(() => this.clear())
);
this.subscriptions.add(
this.resize.pipe(debounceTime(50)).subscribe(() => {
this.alignWrappersToRightIfStickOut();
})
);
}
/**
* During initialization of this component, we will check the resetMenuOnClose flag and attach a menu reset listener if needed.
*/
ngOnInit() {
if (this.resetMenuOnClose) {
this.resetOnMenuCollapse();
}
}
/**
* This method performs the action of resetting the menu (close all sub menus and return to main options)
* when the menu is closed.
*/
resetOnMenuCollapse(): void {
this.subscriptions.add(
this.hamburgerMenuService?.isExpanded
.pipe(distinctUntilChanged(), filter(Boolean))
.subscribe(() => {
this.reinitalizeMenu();
})
);
}
/**
* This method performs the actions required to reset the state of the menu and reset any visual components.
*/
reinitalizeMenu(): void {
if (this.openNodes?.length > 0) {
this.clear();
this.renderer.removeClass(this.elemRef.nativeElement, 'is-open');
}
}
toggleOpen(event: UIEvent): void {
if (event.type === 'keydown') {
event.preventDefault();
}
const node = <HTMLElement>event.currentTarget;
if (this.openNodes.includes(node)) {
if (event.type === 'keydown') {
this.back();
} else {
this.openNodes = this.openNodes.filter((n) => n !== node);
this.renderer.removeClass(node, 'is-open');
}
} else {
this.openNodes.push(node);
}
this.updateClasses();
event.stopImmediatePropagation();
event.stopPropagation();
}
back(): void {
if (this.openNodes[this.openNodes.length - 1]) {
this.renderer.removeClass(
this.openNodes[this.openNodes.length - 1],
'is-open'
);
this.openNodes.pop();
this.updateClasses();
}
}
clear(): void {
this.openNodes = [];
this.updateClasses();
}
onMouseEnter(event: MouseEvent) {
this.alignWrapperToRightIfStickOut(<HTMLElement>event.currentTarget);
this.focusAfterPreviousClicked(event);
}
getTotalDepth(node: NavigationNode, depth = 0): number {
if (node.children && node.children.length > 0) {
return Math.max(
...node.children.map((n) => this.getTotalDepth(n, depth + 1))
);
} else {
return depth;
}
}
getColumnCount(length: number): number {
return Math.round(length / (this.wrapAfter || length));
}
focusAfterPreviousClicked(event: MouseEvent) {
const target: HTMLElement = <HTMLElement>(
(event.target || event.relatedTarget)
);
if (
target.ownerDocument.activeElement.matches('nav[tabindex]') &&
target.parentElement.matches('.flyout')
) {
target.focus();
}
return target.ownerDocument;
}
ngOnDestroy() {
if (this.subscriptions) {
this.subscriptions.unsubscribe();
}
}
private alignWrapperToRightIfStickOut(node: HTMLElement) {
const wrapper = <HTMLElement>node.querySelector('.wrapper');
const body = <HTMLElement>node.closest('body');
if (wrapper) {
this.renderer.removeStyle(wrapper, 'margin-left');
if (
wrapper.offsetLeft + wrapper.offsetWidth >
body.offsetLeft + body.offsetWidth
) {
this.renderer.setStyle(
wrapper,
'margin-left',
`${node.offsetWidth - wrapper.offsetWidth}px`
);
}
}
}
private alignWrappersToRightIfStickOut() {
const navs = <HTMLCollection>this.elemRef.nativeElement.childNodes;
Array.from(navs)
.filter((node) => node.tagName === 'NAV')
.forEach((nav) => this.alignWrapperToRightIfStickOut(<HTMLElement>nav));
}
private updateClasses(): void {
this.openNodes.forEach((node, i) => {
if (i + 1 < this.openNodes.length) {
this.renderer.addClass(node, 'is-opened');
this.renderer.removeClass(node, 'is-open');
} else {
this.renderer.removeClass(node, 'is-opened');
this.renderer.addClass(node, 'is-open');
}
});
this.isOpen = this.openNodes.length > 0;
}
}
<div
*ngIf="flyout && node?.children.length > 1"
class="back is-open"
(click)="back()"
>
<span>
<cx-icon [type]="iconType.CARET_LEFT"></cx-icon>
{{ 'common.back' | cxTranslate }}
</span>
</div>
<ng-container *ngFor="let child of node?.children">
<ng-container *ngTemplateOutlet="nav; context: { node: child, depth: 0 }">
</ng-container>
</ng-container>
<!-- we generate links in a recursive manner -->
<ng-template #nav let-node="node" let-depth="depth">
<nav
(click)="toggleOpen($event)"
(mouseenter)="onMouseEnter($event)"
(keydown.space)="toggleOpen($event)"
(keydown.esc)="back()"
>
<cx-generic-link
*ngIf="
node.url && (!node.children || node.children?.length === 0);
else heading
"
[url]="node.url"
[target]="node.target"
[style]="node.styleAttributes"
[class]="node.styleClasses"
>
{{ node.title }}
<cx-icon
*ngIf="flyout && node.children?.length > 0"
[type]="iconType.CARET_DOWN"
></cx-icon>
</cx-generic-link>
<ng-template #heading>
<span [attr.tabindex]="flyout && (depth === 0 || node.url) ? 0 : -1">
{{ node.title }}
<cx-icon
*ngIf="flyout && node.children?.length > 0"
[type]="iconType.CARET_DOWN"
></cx-icon>
</span>
</ng-template>
<!-- we add a wrapper to allow for better layout handling in CSS -->
<div class="wrapper" *ngIf="node.children?.length > 0">
<cx-generic-link
*ngIf="node.url"
[url]="node.url"
[target]="node.target"
class="all"
>
{{ 'navigation.shopAll' | cxTranslate: { navNode: node.title } }}
</cx-generic-link>
<div
class="childs"
[attr.depth]="getTotalDepth(node)"
[attr.wrap-after]="node.children?.length > wrapAfter ? wrapAfter : null"
[attr.columns]="getColumnCount(node.children?.length)"
>
<ng-container *ngFor="let child of node.children">
<ng-container
*ngTemplateOutlet="nav; context: { node: child, depth: depth + 1 }"
>
</ng-container>
</ng-container>
</div>
</div>
</nav>
</ng-template>