thingsboard/ui-ngx/src/app/shared/components/toast.directive.ts

330 lines
11 KiB
TypeScript
Raw Normal View History

///
2020-02-20 10:26:43 +02:00
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit, ChangeDetectorRef,
Component, ComponentRef,
Directive,
ElementRef,
Inject,
Input,
NgZone,
OnDestroy, Optional,
ViewChild,
ViewContainerRef
} from '@angular/core';
2020-02-10 13:15:29 +02:00
import { MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarConfig, MatSnackBarRef } from '@angular/material/snack-bar';
import { NotificationMessage } from '@app/core/notification/notification.models';
import { Subscription } from 'rxjs';
import { NotificationService } from '@app/core/services/notification.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
2020-02-10 13:04:56 +02:00
import { MatButton } from '@angular/material/button';
import Timeout = NodeJS.Timeout;
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
@Directive({
selector: '[tb-toast]'
})
export class ToastDirective implements AfterViewInit, OnDestroy {
@Input()
toastTarget = 'root';
private notificationSubscription: Subscription = null;
private hideNotificationSubscription: Subscription = null;
private snackBarRef: MatSnackBarRef<any> = null;
private overlayRef: OverlayRef;
private toastComponentRef: ComponentRef<TbSnackBarComponent>;
2020-01-24 19:35:10 +02:00
private currentMessage: NotificationMessage = null;
2020-01-24 19:14:40 +02:00
2020-02-10 13:04:56 +02:00
private dismissTimeout: Timeout = null;
constructor(private elementRef: ElementRef,
private viewContainerRef: ViewContainerRef,
private notificationService: NotificationService,
private overlay: Overlay,
private snackBar: MatSnackBar,
2020-01-24 19:35:10 +02:00
private ngZone: NgZone,
private breakpointObserver: BreakpointObserver,
private cd: ChangeDetectorRef) {
}
ngAfterViewInit(): void {
this.notificationSubscription = this.notificationService.getNotification().subscribe(
(notificationMessage) => {
2020-01-24 19:35:10 +02:00
if (this.shouldDisplayMessage(notificationMessage)) {
this.currentMessage = notificationMessage;
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
if (isGtSm) {
this.showToastPanel(notificationMessage);
} else {
this.showSnackBar(notificationMessage);
}
}
}
);
this.hideNotificationSubscription = this.notificationService.getHideNotification().subscribe(
(hideNotification) => {
if (hideNotification) {
const target = hideNotification.target || 'root';
if (this.toastTarget === target) {
2020-01-24 19:35:10 +02:00
this.ngZone.run(() => {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
if (this.toastComponentRef) {
this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click();
}
2020-01-24 19:35:10 +02:00
});
}
}
}
);
}
private showToastPanel(notificationMessage: NotificationMessage) {
this.ngZone.run(() => {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
const position = this.overlay.position();
let panelClass = ['tb-toast-panel'];
if (notificationMessage.panelClass) {
if (typeof notificationMessage.panelClass === 'string') {
panelClass.push(notificationMessage.panelClass);
} else if (notificationMessage.panelClass.length) {
panelClass = panelClass.concat(notificationMessage.panelClass);
}
}
const overlayConfig = new OverlayConfig({
panelClass,
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: false,
disposeOnNavigation: true
});
let originX;
let originY;
const horizontalPosition = notificationMessage.horizontalPosition || 'left';
const verticalPosition = notificationMessage.verticalPosition || 'top';
if (horizontalPosition === 'start' || horizontalPosition === 'left') {
originX = 'start';
} else if (horizontalPosition === 'end' || horizontalPosition === 'right') {
originX = 'end';
} else {
originX = 'center';
}
if (verticalPosition === 'top') {
originY = 'top';
} else {
originY = 'bottom';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX: originX,
overlayY: originY
};
overlayConfig.positionStrategy = position.flexibleConnectedTo(this.elementRef)
.withPositions([connectedPosition]);
this.overlayRef = this.overlay.create(overlayConfig);
const data: ToastPanelData = {
notification: notificationMessage
};
const injectionTokens = new WeakMap<any, any>([
[MAT_SNACK_BAR_DATA, data],
[OverlayRef, this.overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
this.toastComponentRef = this.overlayRef.attach(new ComponentPortal(TbSnackBarComponent, this.viewContainerRef, injector));
this.cd.detectChanges();
if (notificationMessage.duration && notificationMessage.duration > 0) {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.dismissTimeout = setTimeout(() => {
if (this.toastComponentRef) {
this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click();
}
this.dismissTimeout = null;
}, notificationMessage.duration + 500);
}
this.toastComponentRef.onDestroy(() => {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.overlayRef = null;
this.toastComponentRef = null;
this.currentMessage = null;
});
});
}
private showSnackBar(notificationMessage: NotificationMessage) {
const data: ToastPanelData = {
notification: notificationMessage
};
const config: MatSnackBarConfig = {
horizontalPosition: notificationMessage.horizontalPosition || 'left',
verticalPosition: 'bottom',
viewContainerRef: this.viewContainerRef,
duration: notificationMessage.duration,
panelClass: notificationMessage.panelClass,
data
};
this.ngZone.run(() => {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config);
});
if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.dismissTimeout = setTimeout(() => {
if (this.snackBarRef) {
this.snackBarRef.instance.actionButton._elementRef.nativeElement.click();
}
this.dismissTimeout = null;
}, notificationMessage.duration);
}
this.snackBarRef.afterDismissed().subscribe(() => {
if (this.dismissTimeout !== null) {
clearTimeout(this.dismissTimeout);
this.dismissTimeout = null;
}
this.snackBarRef = null;
this.currentMessage = null;
});
}
2020-01-24 19:35:10 +02:00
private shouldDisplayMessage(notificationMessage: NotificationMessage): boolean {
if (notificationMessage && notificationMessage.message) {
const target = notificationMessage.target || 'root';
if (this.toastTarget === target) {
if (!this.currentMessage || this.currentMessage.message !== notificationMessage.message
|| this.currentMessage.type !== notificationMessage.type) {
return true;
}
}
}
return false;
}
ngOnDestroy(): void {
if (this.overlayRef) {
this.overlayRef.dispose();
}
if (this.notificationSubscription) {
this.notificationSubscription.unsubscribe();
}
if (this.hideNotificationSubscription) {
this.hideNotificationSubscription.unsubscribe();
}
}
}
interface ToastPanelData {
notification: NotificationMessage;
}
import {
AnimationTriggerMetadata,
AnimationEvent,
trigger,
state,
transition,
style,
animate,
} from '@angular/animations';
export const toastAnimations: {
readonly showHideToast: AnimationTriggerMetadata;
} = {
showHideToast: trigger('showHideAnimation', [
state('in', style({ transform: 'scale(1)', opacity: 1 })),
transition('void => opened', [style({ transform: 'scale(0)', opacity: 0 }), animate('{{ open }}ms')]),
transition(
'opened => closing',
animate('{{ close }}ms', style({ transform: 'scale(0)', opacity: 0 })),
),
]),
};
export type ToastAnimationState = 'default' | 'opened' | 'closing';
@Component({
selector: 'tb-snack-bar-component',
templateUrl: 'snack-bar-component.html',
styleUrls: ['snack-bar-component.scss'],
animations: [toastAnimations.showHideToast]
})
export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
2020-02-10 13:04:56 +02:00
@ViewChild('actionButton', {static: true}) actionButton: MatButton;
public notification: NotificationMessage;
animationState: ToastAnimationState;
animationParams = {
open: 100,
close: 100
};
constructor(@Inject(MAT_SNACK_BAR_DATA)
private data: ToastPanelData,
@Optional()
private snackBarRef: MatSnackBarRef<TbSnackBarComponent>,
@Optional()
private overlayRef: OverlayRef) {
this.animationState = !!this.snackBarRef ? 'default' : 'opened';
this.notification = data.notification;
}
ngAfterViewInit() {
}
ngOnDestroy() {
}
action(): void {
if (this.snackBarRef) {
this.snackBarRef.dismissWithAction();
} else {
this.animationState = 'closing';
}
}
onHideFinished(event: AnimationEvent) {
const { toState } = event;
const isFadeOut = (toState as ToastAnimationState) === 'closing';
const itFinished = this.animationState === 'closing';
if (isFadeOut && itFinished) {
this.overlayRef.dispose();
}
}
}