blob: 23c1850ab5201fbea90d1f9250843229a61c70b4 [file] [log] [blame]
/********************************************************************************
* 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;
}
}