/// /// Copyright © 2016-2019 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, Component, Directive, ElementRef, Inject, Input, NgZone, OnDestroy, ViewContainerRef } from '@angular/core'; import { MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarConfig, MatSnackBarRef } from '@angular/material'; import { NotificationMessage } from '@app/core/notification/notification.models'; import { onParentScrollOrWindowResize } from '@app/core/utils'; import { Subscription } from 'rxjs'; import { NotificationService } from '@app/core/services/notification.service'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; @Directive({ selector: '[tb-toast]' }) export class ToastDirective implements AfterViewInit, OnDestroy { @Input() toastTarget = 'root'; private notificationSubscription: Subscription = null; private hideNotificationSubscription: Subscription = null; private snackBarRef: MatSnackBarRef = null; private currentMessage: NotificationMessage = null; constructor(public elementRef: ElementRef, public viewContainerRef: ViewContainerRef, private notificationService: NotificationService, public snackBar: MatSnackBar, private ngZone: NgZone, private breakpointObserver: BreakpointObserver) { } ngAfterViewInit(): void { this.notificationSubscription = this.notificationService.getNotification().subscribe( (notificationMessage) => { if (this.shouldDisplayMessage(notificationMessage)) { this.currentMessage = notificationMessage; const data = { parent: this.elementRef, notification: notificationMessage }; const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); const config: MatSnackBarConfig = { horizontalPosition: notificationMessage.horizontalPosition || 'left', verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'), viewContainerRef: this.viewContainerRef, duration: notificationMessage.duration, data }; this.ngZone.run(() => { if (this.snackBarRef) { this.snackBarRef.dismiss(); } this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config); this.snackBarRef.afterDismissed().subscribe(() => { this.snackBarRef = null; this.currentMessage = null; }); }); } } ); 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(); } }); } } } ); } 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.notificationSubscription) { this.notificationSubscription.unsubscribe(); } if (this.hideNotificationSubscription) { this.hideNotificationSubscription.unsubscribe(); } } } @Component({ selector: 'tb-snack-bar-component', templateUrl: 'snack-bar-component.html', styleUrls: ['snack-bar-component.scss'] }) export class TbSnackBarComponent implements AfterViewInit, OnDestroy { private parentEl: HTMLElement; private snackBarContainerEl: HTMLElement; private parentScrollSubscription: Subscription = null; public notification: NotificationMessage; constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any, private elementRef: ElementRef, public snackBarRef: MatSnackBarRef) { this.notification = data.notification; } ngAfterViewInit() { this.parentEl = this.data.parent.nativeElement; this.snackBarContainerEl = this.elementRef.nativeElement.parentNode; this.snackBarContainerEl.style.position = 'absolute'; this.updateContainerRect(); this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig); const snackBarComponent = this; this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => { snackBarComponent.updateContainerRect(); }); } 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'; } } ngOnDestroy() { if (this.parentScrollSubscription) { this.parentScrollSubscription.unsubscribe(); } } 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'; } action(): void { this.snackBarRef.dismissWithAction(); } }