blob: 74d7a8ca7513a5ea0abd173a917f10b2c02d7454 [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 {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewChild
} from "@angular/core";
import {BehaviorSubject, defer, timer} from "rxjs";
import {distinctUntilChanged, skip, switchMap, tap} from "rxjs/operators";
@Component({
selector: "app-collapsible",
templateUrl: "./collapsible.component.html",
styleUrls: ["./collapsible.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CollapsibleComponent implements OnInit, OnDestroy {
/**
* Sets the transition time in milliseconds for collapsing or expanding the box
*/
@Input()
public appTransitionTime = 200;
/**
* Sets the title of the container
*/
@Input()
public appTitle: string;
/**
* If set, the template is rendered is rendered in the header part of the container
*/
@Input()
public appHeaderTemplateRef: TemplateRef<any>;
/**
* If set, shows a simpler version of the collapsible.
* No borders and colors will be shown for the header and will only collapse when clicking on the button, not the whole row.
*/
@Input()
public appSimpleCollapsible = false;
@Input()
public appDisabled: boolean;
@Output()
public readonly appCollapsedChange = new EventEmitter<boolean>();
/**
* This observables returns true if and only if the container is collapsed
*/
public isCollapsed$ = defer(() => this.isCollapsedSubject.asObservable());
/**
* On subscribing, this observables sets the height of the component's native element to zero.
* Note that the animation works only, if the native element has it's height set to an absolute value.
*/
private collapse$ = defer(() => {
this.setHeight(this.getScrollHeight() + "px");
return timer(20); // This timeout is necessary so that the height is set on the element.
}).pipe(
tap(() => this.setHeight("0px")),
switchMap(() => timer(this.appTransitionTime))
);
/**
* On subscribing, this observables sets the height of the component's native element to zero.
* Note that the animation works only, if the native element has it's height set to an absolute value.
*/
private expand$ = defer(() => {
this.setHeight(this.getScrollHeight() + "px");
return timer(this.appTransitionTime);
}).pipe(tap(() => this.setHeight("initial")));
@ViewChild("bodyElement")
private bodyElementRef: ElementRef<HTMLElement>;
private isCollapsedSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public constructor(
public readonly elementRef: ElementRef<HTMLElement>
) {
}
/**
* Returns the transition duration of the component's native element
*/
@HostBinding("style.transition-duration")
public get transition(): string {
return `${this.appTransitionTime}ms`;
}
/**
* Sets the state of the container
*/
@Input()
public set appCollapsed(collapse: boolean) {
this.toggle(collapse);
}
public ngOnInit() {
if (this.isCollapsedSubject.getValue()) {
this.setHeight("0");
}
this.isCollapsed$.pipe(
distinctUntilChanged(),
skip(1),
switchMap((collapse) => collapse ? this.collapse$ : this.expand$),
tap(() => this.appCollapsedChange.emit(this.isCollapsedSubject.getValue())),
).subscribe();
}
public ngOnDestroy() {
this.isCollapsedSubject.complete();
}
/**
* Toggles between the states of the container
* @param collapse If null, the state is switched to the opposite of the current state
*/
public toggle(collapse: boolean = !this.isCollapsedSubject.getValue()) {
this.isCollapsedSubject.next(collapse);
}
/**
* Sets the height of the component's native element (is overwritten while animating)
*/
public setHeight(height: string) {
try {
this.elementRef.nativeElement.style.height = height;
} catch (e) {
return;
}
}
/**
* Computes the scroll height of the component's native element
*/
public getScrollHeight(): number {
try {
return this.elementRef.nativeElement.scrollHeight;
} catch (err) {
return 0;
}
}
}