| /******************************************************************************** |
| * 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; |
| } |
| } |
| |
| } |