377 lines
13 KiB
TypeScript
377 lines
13 KiB
TypeScript
///
|
|
/// Copyright © 2016-2021 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, ComponentFactoryResolver, ComponentRef,
|
|
Directive,
|
|
ElementRef, HostBinding,
|
|
Inject,
|
|
Injector,
|
|
Input,
|
|
NgZone,
|
|
OnDestroy, Optional,
|
|
StaticProvider,
|
|
ViewChild,
|
|
ViewContainerRef
|
|
} from '@angular/core';
|
|
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';
|
|
import { MatButton } from '@angular/material/button';
|
|
import Timeout = NodeJS.Timeout;
|
|
|
|
@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 toastComponentRef: ComponentRef<TbSnackBarComponent>;
|
|
private currentMessage: NotificationMessage = null;
|
|
|
|
private dismissTimeout: Timeout = null;
|
|
|
|
constructor(private elementRef: ElementRef,
|
|
private viewContainerRef: ViewContainerRef,
|
|
private notificationService: NotificationService,
|
|
private componentFactoryResolver: ComponentFactoryResolver,
|
|
private snackBar: MatSnackBar,
|
|
private ngZone: NgZone,
|
|
private breakpointObserver: BreakpointObserver,
|
|
private cd: ChangeDetectorRef) {
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
this.notificationSubscription = this.notificationService.getNotification().subscribe(
|
|
(notificationMessage) => {
|
|
if (this.shouldDisplayMessage(notificationMessage)) {
|
|
this.currentMessage = notificationMessage;
|
|
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
|
|
if (isGtSm && this.toastTarget !== 'root') {
|
|
this.showToastPanel(notificationMessage);
|
|
} else {
|
|
this.showSnackBar(notificationMessage, isGtSm);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
this.hideNotificationSubscription = this.notificationService.getHideNotification().subscribe(
|
|
(hideNotification) => {
|
|
if (hideNotification) {
|
|
const target = hideNotification.target || 'root';
|
|
if (this.toastTarget === target) {
|
|
this.ngZone.run(() => {
|
|
if (this.snackBarRef) {
|
|
this.snackBarRef.dismiss();
|
|
}
|
|
if (this.toastComponentRef) {
|
|
this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
private showToastPanel(notificationMessage: NotificationMessage) {
|
|
this.ngZone.run(() => {
|
|
if (this.snackBarRef) {
|
|
this.snackBarRef.dismiss();
|
|
}
|
|
if (this.toastComponentRef) {
|
|
this.viewContainerRef.detach(0);
|
|
this.toastComponentRef.destroy();
|
|
}
|
|
let panelClass = ['tb-toast-panel', '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 horizontalPosition = notificationMessage.horizontalPosition || 'left';
|
|
const verticalPosition = notificationMessage.verticalPosition || 'top';
|
|
if (horizontalPosition === 'start' || horizontalPosition === 'left') {
|
|
panelClass.push('left');
|
|
} else if (horizontalPosition === 'end' || horizontalPosition === 'right') {
|
|
panelClass.push('right');
|
|
} else {
|
|
panelClass.push('h-center');
|
|
}
|
|
if (verticalPosition === 'top') {
|
|
panelClass.push('top');
|
|
} else {
|
|
panelClass.push('bottom');
|
|
}
|
|
|
|
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(TbSnackBarComponent);
|
|
const data: ToastPanelData = {
|
|
notification: notificationMessage,
|
|
panelClass,
|
|
destroyToastComponent: () => {
|
|
this.viewContainerRef.detach(0);
|
|
this.toastComponentRef.destroy();
|
|
}
|
|
};
|
|
const providers: StaticProvider[] = [
|
|
{provide: MAT_SNACK_BAR_DATA, useValue: data}
|
|
];
|
|
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
|
|
this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, 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.toastComponentRef = null;
|
|
this.currentMessage = null;
|
|
});
|
|
});
|
|
}
|
|
|
|
private showSnackBar(notificationMessage: NotificationMessage, isGtSm: boolean) {
|
|
this.ngZone.run(() => {
|
|
if (this.snackBarRef) {
|
|
this.snackBarRef.dismiss();
|
|
}
|
|
const data: ToastPanelData = {
|
|
notification: notificationMessage,
|
|
parent: this.elementRef,
|
|
panelClass: [],
|
|
destroyToastComponent: () => {}
|
|
};
|
|
const config: MatSnackBarConfig = {
|
|
horizontalPosition: notificationMessage.horizontalPosition || 'left',
|
|
verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'),
|
|
viewContainerRef: this.viewContainerRef,
|
|
duration: notificationMessage.duration,
|
|
panelClass: notificationMessage.panelClass,
|
|
data
|
|
};
|
|
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;
|
|
});
|
|
});
|
|
}
|
|
|
|
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.toastComponentRef) {
|
|
this.viewContainerRef.detach(0);
|
|
this.toastComponentRef.destroy();
|
|
}
|
|
if (this.notificationSubscription) {
|
|
this.notificationSubscription.unsubscribe();
|
|
}
|
|
if (this.hideNotificationSubscription) {
|
|
this.hideNotificationSubscription.unsubscribe();
|
|
}
|
|
}
|
|
}
|
|
|
|
interface ToastPanelData {
|
|
notification: NotificationMessage;
|
|
parent?: ElementRef;
|
|
panelClass: string[];
|
|
destroyToastComponent: () => void;
|
|
}
|
|
|
|
import {
|
|
AnimationTriggerMetadata,
|
|
AnimationEvent,
|
|
trigger,
|
|
state,
|
|
transition,
|
|
style,
|
|
animate,
|
|
} from '@angular/animations';
|
|
import { onParentScrollOrWindowResize } from '@core/utils';
|
|
|
|
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 {
|
|
|
|
@ViewChild('actionButton', {static: true}) actionButton: MatButton;
|
|
|
|
@HostBinding('class')
|
|
get panelClass(): string[] {
|
|
return this.data.panelClass;
|
|
}
|
|
|
|
private parentEl: HTMLElement;
|
|
private snackBarContainerEl: HTMLElement;
|
|
private parentScrollSubscription: Subscription = null;
|
|
|
|
public notification: NotificationMessage;
|
|
|
|
animationState: ToastAnimationState;
|
|
|
|
animationParams = {
|
|
open: 100,
|
|
close: 100
|
|
};
|
|
|
|
constructor(@Inject(MAT_SNACK_BAR_DATA)
|
|
private data: ToastPanelData,
|
|
private elementRef: ElementRef,
|
|
@Optional()
|
|
private snackBarRef: MatSnackBarRef<TbSnackBarComponent>) {
|
|
this.animationState = !!this.snackBarRef ? 'default' : 'opened';
|
|
this.notification = data.notification;
|
|
}
|
|
|
|
ngAfterViewInit() {
|
|
if (this.snackBarRef) {
|
|
this.parentEl = this.data.parent.nativeElement;
|
|
this.snackBarContainerEl = $(this.elementRef.nativeElement).closest('snack-bar-container')[0];
|
|
this.snackBarContainerEl.style.position = 'absolute';
|
|
this.updateContainerRect();
|
|
this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig);
|
|
this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => {
|
|
this.updateContainerRect();
|
|
});
|
|
}
|
|
}
|
|
|
|
private updatePosition(config: MatSnackBarConfig) {
|
|
const isRtl = config.direction === 'rtl';
|
|
const isLeft = (config.horizontalPosition === 'left' ||
|
|
(config.horizontalPosition === 'start' && !isRtl) ||
|
|
(config.horizontalPosition === 'end' && isRtl));
|
|
const isRight = !isLeft && config.horizontalPosition !== 'center';
|
|
if (isLeft) {
|
|
this.snackBarContainerEl.style.justifyContent = 'flex-start';
|
|
} else if (isRight) {
|
|
this.snackBarContainerEl.style.justifyContent = 'flex-end';
|
|
} else {
|
|
this.snackBarContainerEl.style.justifyContent = 'center';
|
|
}
|
|
if (config.verticalPosition === 'top') {
|
|
this.snackBarContainerEl.style.alignItems = 'flex-start';
|
|
} else {
|
|
this.snackBarContainerEl.style.alignItems = 'flex-end';
|
|
}
|
|
}
|
|
|
|
private updateContainerRect() {
|
|
const viewportOffset = this.parentEl.getBoundingClientRect();
|
|
this.snackBarContainerEl.style.top = viewportOffset.top + 'px';
|
|
this.snackBarContainerEl.style.left = viewportOffset.left + 'px';
|
|
this.snackBarContainerEl.style.width = viewportOffset.width + 'px';
|
|
this.snackBarContainerEl.style.height = viewportOffset.height + 'px';
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
if (this.parentScrollSubscription) {
|
|
this.parentScrollSubscription.unsubscribe();
|
|
}
|
|
}
|
|
|
|
action(event: MouseEvent): void {
|
|
event.stopPropagation();
|
|
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.data.destroyToastComponent();
|
|
}
|
|
}
|
|
}
|