File

projects/storefrontlib/cms-components/navigation/navigation/navigation-ui.component.ts

Implements

OnInit OnDestroy

Metadata

changeDetection ChangeDetectionStrategy.OnPush
selector cx-navigation-ui
templateUrl ./navigation-ui.component.html

Index

Properties
Methods
Inputs
HostListeners

Constructor

constructor(router: Router, renderer: Renderer2, elemRef: ElementRef, hamburgerMenuService: HamburgerMenuService)
Parameters :
Name Type Optional
router Router No
renderer Renderer2 No
elemRef ElementRef No
hamburgerMenuService HamburgerMenuService No

Inputs

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.

HostListeners

window:resize
window:resize()

Methods

Private alignWrappersToRightIfStickOut
alignWrappersToRightIfStickOut()
Returns : void
Private alignWrapperToRightIfStickOut
alignWrapperToRightIfStickOut(node: HTMLElement)
Parameters :
Name Type Optional
node HTMLElement No
Returns : void
back
back()
Returns : void
clear
clear()
Returns : void
focusAfterPreviousClicked
focusAfterPreviousClicked(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : any
getColumnCount
getColumnCount(length: number)
Parameters :
Name Type Optional
length number No
Returns : number
getTotalDepth
getTotalDepth(node: NavigationNode, depth: number)
Parameters :
Name Type Optional Default value
node NavigationNode No
depth number No 0
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 :
Name Type Optional
event MouseEvent No
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 :
Name Type Optional
event UIEvent No
Returns : void
Private updateClasses
updateClasses()
Returns : void

Properties

flyout
Default value : true
Decorators :
@Input()
@HostBinding('class.flyout')

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.

iconType
Default value : ICON_TYPE

the icon type that will be used for navigation nodes with children.

isOpen
Default value : false
Decorators :
@Input()
@HostBinding('class.is-open')
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>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""