| /******************************************************************************** |
| * Copyright (c) 2020 Contributors to the Eclipse Foundation |
| * |
| * See the NOTICE file(s) distributed with this work for additional |
| * information regarding copyright ownership. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0 |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| ********************************************************************************/ |
| |
| import {ConnectedPosition, Overlay, OverlayConfig, OverlayRef, PositionStrategy} from "@angular/cdk/overlay"; |
| import {TemplatePortal} from "@angular/cdk/portal"; |
| import {DOCUMENT} from "@angular/common"; |
| import {Directive, ElementRef, EventEmitter, Inject, Input, OnDestroy, Output, TemplateRef, ViewContainerRef} from "@angular/core"; |
| import {fromEvent, merge, Subscription, timer} from "rxjs"; |
| import {filter, take} from "rxjs/operators"; |
| import {WINDOW} from "../../../core"; |
| import {EKeyboardKeys} from "../../../util/events"; |
| |
| @Directive({ |
| selector: "[appDropDown]", |
| exportAs: "appDropDown" |
| }) |
| export class DropDownDirective<C = any> implements OnDestroy { |
| |
| @Input() |
| public appConnectedPositions: ConnectedPosition[] = [ |
| { |
| originX: "center", |
| originY: "bottom", |
| overlayX: "center", |
| overlayY: "top", |
| panelClass: "bottom" |
| }, |
| { |
| originX: "center", |
| originY: "top", |
| overlayX: "center", |
| overlayY: "bottom", |
| panelClass: "top" |
| }, |
| { |
| originX: "end", |
| originY: "top", |
| overlayX: "start", |
| overlayY: "top", |
| panelClass: "right" |
| }, |
| { |
| originX: "start", |
| originY: "top", |
| overlayX: "end", |
| overlayY: "top", |
| panelClass: "left" |
| } |
| ]; |
| |
| @Input() |
| public appPreventAutomaticClose = false; |
| |
| @Input() |
| public appDropDown: TemplateRef<C>; |
| |
| @Input() |
| public appFixedWidth: string; |
| |
| @Output() |
| public readonly appOpen = new EventEmitter<Event | null>(); |
| |
| @Output() |
| public readonly appClose = new EventEmitter<Event | null>(); |
| |
| public isOpen = false; |
| |
| public position: ConnectedPosition; |
| |
| private disabled = false; |
| |
| private overlayRef: OverlayRef; |
| |
| private subscriptionToClose: Subscription; |
| |
| constructor( |
| private readonly elementRef: ElementRef<HTMLElement>, |
| private readonly viewContainerRef: ViewContainerRef, |
| private readonly overlay: Overlay, |
| @Inject(DOCUMENT) private readonly document: Document, |
| @Inject(WINDOW) private readonly window: Window |
| ) { |
| |
| } |
| |
| public ngOnDestroy(): void { |
| this.close(); |
| } |
| |
| @Input() |
| public set appDisabled(value: boolean) { |
| this.disabled = value; |
| if (value) { |
| timer(0).toPromise().then(() => this.close()); |
| } |
| } |
| |
| public get nativeElement(): HTMLElement { |
| return this.elementRef?.nativeElement; |
| } |
| |
| public get overlayElement(): HTMLElement { |
| return this.overlayRef?.overlayElement; |
| } |
| |
| public isElementContainedIn(element?: Element | any): boolean { |
| return this.isElementContainedInOverlay(element) || this.isElementContainedInOrigin(element); |
| } |
| |
| public isElementContainedInOrigin(element?: Element | any): boolean { |
| return this.nativeElement?.contains(element); |
| } |
| |
| public isElementContainedInOverlay(element?: Element | any): boolean { |
| return this.overlayElement?.contains(element); |
| } |
| |
| public toggle(openOrClose?: boolean) { |
| openOrClose = openOrClose == null ? this.overlayRef == null : openOrClose; |
| return openOrClose ? this.open() : this.close(); |
| } |
| |
| public async open() { |
| if (!this.disabled && this.overlayRef == null && this.appDropDown != null) { |
| this.overlayRef = this.overlay.create(this.getOverlayConfig()); |
| const portal = new TemplatePortal(this.appDropDown, this.viewContainerRef); |
| this.overlayRef.attach(portal); |
| this.overlayRef.detachments().pipe(take(1)).subscribe(() => this.close()); |
| this.subscribeToClose(); |
| this.isOpen = this.overlayRef != null; |
| this.appOpen.emit(null); |
| } |
| } |
| |
| public async close(event?: Event) { |
| if (this.overlayRef != null) { |
| this.unsubscribeToClose(); |
| this.overlayRef.dispose(); |
| this.overlayRef = null; |
| this.isOpen = false; |
| this.appClose.emit(event); |
| } |
| } |
| |
| public async updatePosition() { |
| if (this.overlayRef != null) { |
| this.overlayRef.updatePosition(); |
| } |
| } |
| |
| private subscribeToClose() { |
| this.unsubscribeToClose(); |
| if (this.overlayRef != null) { |
| this.subscriptionToClose = merge<Event>( |
| fromEvent(this.document, "click").pipe( |
| filter((event) => !this.isElementContainedIn(event?.target)) |
| ), |
| fromEvent(this.window, "resize"), |
| fromEvent(this.nativeElement, "resize"), |
| fromEvent<KeyboardEvent>(this.document, "keydown").pipe( |
| filter((event) => event?.key === EKeyboardKeys.ESCAPE) |
| ) |
| ).subscribe((event: Event) => this.close(event), (error) => console.error(error)); |
| } |
| } |
| |
| private unsubscribeToClose() { |
| if (this.subscriptionToClose != null) { |
| this.subscriptionToClose.unsubscribe(); |
| } |
| } |
| |
| private getOverlayConfig(): OverlayConfig { |
| const origin = this.elementRef.nativeElement; |
| const rect = origin.getBoundingClientRect(); |
| return new OverlayConfig({ |
| width: this.appFixedWidth ? "unset" : rect.width, |
| maxWidth: this.appFixedWidth ? this.appFixedWidth : undefined, |
| positionStrategy: this.getPositionStrategy(origin), |
| scrollStrategy: this.overlay.scrollStrategies.close() |
| }); |
| } |
| |
| private getPositionStrategy(origin: HTMLElement): PositionStrategy { |
| const result = this.overlay.position() |
| .flexibleConnectedTo(origin) |
| .withPositions(this.appConnectedPositions) |
| .withFlexibleDimensions(true) |
| .withViewportMargin(8) |
| .withPush(true); |
| |
| result.positionChanges.subscribe({ |
| next: (event) => this.position = event?.connectionPair, |
| error: () => this.position = null, |
| complete: () => this.position = null |
| }); |
| |
| return result; |
| } |
| |
| } |