/*
* © 2017 Stratio Big Data Inc., Sucursal en España.
*
* This software is licensed under the Apache License, Version 2.0.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the terms of the License for more details.
*
* SPDX-License-Identifier: Apache-2.0.
*/
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
SimpleChanges,
ViewChild
} from '@angular/core';
import { StPopOffset, StPopPlacement } from '../st-pop/st-pop.model';
import { ARROW_KEY_CODE, StDropDownMenuGroup, StDropDownMenuItem, StDropDownVisualMode } from './st-dropdown-menu.interface';
/**
* @description {Component} [Dropdown Menu]
* This directive show a dropdown menu list in element that you attach
*
*
* @model
*
* [Menu items] {./st-dropdown-menu.interface.ts#StDropDownMenuItem}
*
* @example
*
* {html}
*
* ```
*
*
*
* ```
*/
@Component({
selector: 'st-dropdown-menu',
templateUrl: './st-dropdown-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StDropdownMenuComponent implements AfterViewInit, OnInit, OnChanges, OnDestroy {
/** @Input {boolean} [active=false] Show or hide list */
@Input() active: boolean = false;
/** @Input {StDropDownMenuItem[] | StDropDownMenuGroup[]} [items=\[\]] List of items or groups of them to show in menu */
@Input() items: StDropDownMenuItem[] | StDropDownMenuGroup[] = [];
/* tslint:disable-next-line:max-line-length */
/** @Input {StPopPlacement} [placement=StPopPlacement.BOTTOM_START] Possible positions of menu with respect element to attach */
@Input() placement: StPopPlacement = StPopPlacement.BOTTOM_START;
/** @Input {string} [emptyListMessage=''] Message to show in case of empty list */
@Input() emptyListMessage: string = '';
/** @Input {StDropDownMenuItem | undefined} [selectedItem=undefined] Define selected item without passing as property */
@Input() selectedItem: StDropDownMenuItem = undefined;
/** @Input {StDropDownMenuItem | undefined} [itemsBeforeScroll=undefined] Define selected item without passing as property */
@Input() itemsBeforeScroll: number = 8;
/** @Input {boolean} [moveSelected=true] If true, move selected item to top in menu when open */
@Input() moveSelected: boolean = true;
/** @Input {boolean} [styleSelected=true] If true, apply class selected to selected item */
@Input() styleSelected: boolean = true;
/** @Input {boolean} [isLoading=true] If true, show loader at the end of the list */
@Input() isLoading: boolean = false;
/** @Input {StPopOffset} [offset={x: 0 , y: 0}] For position with offset in x o y axis */
@Input() offset: StPopOffset = { x: 0, y: 0 };
/** @Input {boolean} [openToLeft=false] For calculating all positions from the right corner */
@Input() openToLeft: boolean = false;
/** @Input {StDropdownVisualMode} [visualMode=StDropDownVisualMode.OPTION_LIST] It is needed to specify the styles applied to the list.
* By default is displayed as a normal option list
*/
@Input() visualMode: StDropDownVisualMode = StDropDownVisualMode.OPTION_LIST;
/** @Input {boolean} [keyBoardMove=false] It is needed to activate navigation through options using the keyboard
*/
@Input() keyBoardMove: boolean = false;
/** @output {StDropDownMenuItem} [change] Event emitted when user select an item */
@Output() change: EventEmitter = new EventEmitter();
/** @output {any} [scrollAtBottom] Event emitted when scroll reach the end of the list */
@Output() scrollAtBottom: EventEmitter = new EventEmitter();
/** @output {StDropDownMenuItem} [itemMouseEnter] Event emitted when mouse is over an item */
@Output() itemMouseEnter: EventEmitter = new EventEmitter();
/** @output {StDropDownMenuItem} [itemMouseLeave] Event emitted when mouse leaves an item */
@Output() itemMouseLeave: EventEmitter = new EventEmitter();
@ViewChild('buttonId', { static: false }) buttonElement: ElementRef;
@ViewChild('itemList', { static: false }) itemListElement: ElementRef;
widthMenu: string = '0px';
private _itemHeight: number = 42;
private _focusedOptionPos: number = -1;
private _focusListenerFn: () => void;
constructor(private el: ElementRef, private cd: ChangeDetectorRef, private renderer: Renderer2) {
}
ngOnInit(): void {
if (this.keyBoardMove) {
this._focusListenerFn = this.renderer.listen('document', 'keydown', this.arrowKeyListener.bind(this));
}
}
get componentId(): string | null {
const id = (this.el.nativeElement as HTMLElement).getAttribute('id');
return id !== undefined && id !== null ? id : null;
}
get menuId(): string | null {
return this.componentId !== null ? `${this.componentId}-menu` : null;
}
get isItemGroup(): boolean {
return this.isDropDownGroup(this.items);
}
get menuMaxHeight(): string {
return this.itemsBeforeScroll ? `${this._itemHeight * this.itemsBeforeScroll}px` : null;
}
get listClasses(): any {
return { 'st-dropdown-menu': true, 'active': this.active, 'menu-list': this.displayAsMenuList() };
}
getItemId(value: any | undefined): string | null {
return this.componentId !== null && value !== undefined ? `${this.componentId}-option-${this.getItemValueMerged(value)}` : null;
}
isDropDownGroup(value: StDropDownMenuItem[] | StDropDownMenuGroup[]): value is StDropDownMenuGroup[] {
return value && value.length > 0 && (value[0]).title !== undefined;
}
ngAfterViewInit(): void {
this.updateWidth();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes.active && changes.active.currentValue && this.selectedItem && this.moveSelected) {
// Only can do this functionality with timeout because we need to wait for angular to load new DOM
// with items before move scroll
setTimeout(() => {
if (this.itemListElement) {
const parent: HTMLElement = this.itemListElement.nativeElement;
const listOfElements: any = parent.getElementsByClassName('selected');
if (listOfElements && listOfElements.length > 0) {
const target: HTMLElement = (listOfElements.item(0) as HTMLElement);
parent.scrollTop = target.offsetTop - parent.offsetTop;
}
}
});
} else {
if (changes && changes.active && !changes.active.currentValue) {
this._focusedOptionPos = -1;
}
}
}
onChange(value: StDropDownMenuItem): void {
this.change.emit(value);
}
onHandleScroll(): void {
const element = this.itemListElement.nativeElement;
if (element.scrollHeight - element.scrollTop === element.clientHeight && !this.isLoading) {
this.scrollAtBottom.emit();
}
}
onMouseEnter(item: StDropDownMenuItem): void {
this.itemMouseEnter.emit(item);
}
onMouseLeave(item: StDropDownMenuItem): void {
this.itemMouseLeave.emit(item);
}
displayAsMenuList(): boolean {
return this.visualMode === StDropDownVisualMode.MENU_LIST;
}
@HostListener('window:resize')
@HostListener('window:load')
updateWidth(): void {
const button: HTMLElement = this.buttonElement.nativeElement;
setTimeout(() => {
if (button.children && button.children.length > 0) {
this.widthMenu = button.children[0].getBoundingClientRect().width + 'px';
} else {
this.widthMenu = button.getBoundingClientRect().width + 'px';
}
this.cd.markForCheck();
});
}
ngOnDestroy(): void {
if (this._focusListenerFn) {
this._focusListenerFn();
}
}
private getItemValueMerged(value: any): string {
return value.toString().replace(/\s+/g, '_');
}
private getSelectedItemPosition(): number {
if (this.selectedItem && this.items) {
let _items: StDropDownMenuItem[] = [];
if (this.isDropDownGroup(this.items)) {
this.items.forEach((item: StDropDownMenuItem | StDropDownMenuGroup) => {
if (( item).items) {
_items.push(...( item).items);
} else {
_items.push(( item));
}
});
} else {
_items = this.items;
}
return _items.findIndex(item => item.value === this.selectedItem.value);
} else {
return -1;
}
}
private arrowKeyListener(event: KeyboardEvent): void {
const selectedItemPosition = this.getSelectedItemPosition();
if (selectedItemPosition > -1 && this._focusedOptionPos < 0) {
this._focusedOptionPos = selectedItemPosition;
}
let nextFocus: number;
if (event.key === ARROW_KEY_CODE.ARROW_DOWN || event.key === ARROW_KEY_CODE.ARROW_UP) {
event.preventDefault();
const options: HTMLLIElement[] = this.el.nativeElement.querySelectorAll('.st-dropdown-menu-item');
nextFocus = (event.key === ARROW_KEY_CODE.ARROW_DOWN || this._focusedOptionPos === -1) ? 1 : -1;
this._focusedOptionPos = this._focusedOptionPos + nextFocus;
if (this._focusedOptionPos < 0) {
this._focusedOptionPos = options.length - 1;
} else if (this._focusedOptionPos > options.length - 1) {
this._focusedOptionPos = 0;
}
if (options[this._focusedOptionPos]) {
options[this._focusedOptionPos].focus();
}
}
this.cd.markForCheck();
}
}