Commit e5e103be98957937a585b1d28be28f469898605a

Authored by Igor Kulikov
1 parent d166be02

Improve toast panel

... ... @@ -25,5 +25,5 @@
25 25 'info-toast': notification.type === 'info'
26 26 }">
27 27 <div class="toast-text" [innerHTML]="notification.message"></div>
28   - <button #actionButton mat-button (click)="action()">{{ 'action.close' | translate }}</button>
  28 + <button #actionButton type="button" mat-button (click)="action()">{{ 'action.close' | translate }}</button>
29 29 </div>
... ...
... ... @@ -16,6 +16,33 @@
16 16 :host {
17 17 display: inline-block;
18 18 pointer-events: all;
  19 + &.toast-panel {
  20 + position: absolute;
  21 + top: 0;
  22 + bottom: 0;
  23 + left: 0;
  24 + right: 0;
  25 + z-index: 1000;
  26 + display: flex;
  27 + &.left {
  28 + justify-content: flex-start;
  29 + }
  30 + &.right {
  31 + justify-content: flex-end;
  32 + }
  33 + &.top {
  34 + align-items: flex-start;
  35 + }
  36 + &.bottom {
  37 + align-items: flex-end;
  38 + }
  39 + &.h-center {
  40 + justify-content: center;
  41 + }
  42 + &.v-center {
  43 + align-items: center;
  44 + }
  45 + }
19 46 .tb-toast {
20 47 box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
21 48 color: #fff;
... ...
... ... @@ -16,9 +16,9 @@
16 16
17 17 import {
18 18 AfterViewInit, ChangeDetectorRef,
19   - Component, ComponentRef,
  19 + Component, ComponentFactoryResolver, ComponentRef,
20 20 Directive,
21   - ElementRef,
  21 + ElementRef, HostBinding,
22 22 Inject,
23 23 Input,
24 24 NgZone,
... ... @@ -34,8 +34,7 @@ import { BreakpointObserver } from '@angular/cdk/layout';
34 34 import { MediaBreakpoints } from '@shared/models/constants';
35 35 import { MatButton } from '@angular/material/button';
36 36 import Timeout = NodeJS.Timeout;
37   -import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
38   -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  37 +import { PortalInjector } from '@angular/cdk/portal';
39 38
40 39 @Directive({
41 40 selector: '[tb-toast]'
... ... @@ -49,7 +48,6 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
49 48 private hideNotificationSubscription: Subscription = null;
50 49
51 50 private snackBarRef: MatSnackBarRef<any> = null;
52   - private overlayRef: OverlayRef;
53 51 private toastComponentRef: ComponentRef<TbSnackBarComponent>;
54 52 private currentMessage: NotificationMessage = null;
55 53
... ... @@ -58,7 +56,7 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
58 56 constructor(private elementRef: ElementRef,
59 57 private viewContainerRef: ViewContainerRef,
60 58 private notificationService: NotificationService,
61   - private overlay: Overlay,
  59 + private componentFactoryResolver: ComponentFactoryResolver,
62 60 private snackBar: MatSnackBar,
63 61 private ngZone: NgZone,
64 62 private breakpointObserver: BreakpointObserver,
... ... @@ -104,9 +102,11 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
104 102 if (this.snackBarRef) {
105 103 this.snackBarRef.dismiss();
106 104 }
107   -
108   - const position = this.overlay.position();
109   - let panelClass = ['tb-toast-panel'];
  105 + if (this.toastComponentRef) {
  106 + this.viewContainerRef.detach(0);
  107 + this.toastComponentRef.destroy();
  108 + }
  109 + let panelClass = ['tb-toast-panel', 'toast-panel'];
110 110 if (notificationMessage.panelClass) {
111 111 if (typeof notificationMessage.panelClass === 'string') {
112 112 panelClass.push(notificationMessage.panelClass);
... ... @@ -114,46 +114,35 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
114 114 panelClass = panelClass.concat(notificationMessage.panelClass);
115 115 }
116 116 }
117   - const overlayConfig = new OverlayConfig({
118   - panelClass,
119   - backdropClass: 'cdk-overlay-transparent-backdrop',
120   - hasBackdrop: false,
121   - disposeOnNavigation: true
122   - });
123   - let originX;
124   - let originY;
125 117 const horizontalPosition = notificationMessage.horizontalPosition || 'left';
126 118 const verticalPosition = notificationMessage.verticalPosition || 'top';
127 119 if (horizontalPosition === 'start' || horizontalPosition === 'left') {
128   - originX = 'start';
  120 + panelClass.push('left');
129 121 } else if (horizontalPosition === 'end' || horizontalPosition === 'right') {
130   - originX = 'end';
  122 + panelClass.push('right');
131 123 } else {
132   - originX = 'center';
  124 + panelClass.push('h-center');
133 125 }
134 126 if (verticalPosition === 'top') {
135   - originY = 'top';
  127 + panelClass.push('top');
136 128 } else {
137   - originY = 'bottom';
  129 + panelClass.push('bottom');
138 130 }
139   - const connectedPosition: ConnectedPosition = {
140   - originX,
141   - originY,
142   - overlayX: originX,
143   - overlayY: originY
144   - };
145   - overlayConfig.positionStrategy = position.flexibleConnectedTo(this.elementRef)
146   - .withPositions([connectedPosition]);
147   - this.overlayRef = this.overlay.create(overlayConfig);
  131 +
  132 + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(TbSnackBarComponent);
148 133 const data: ToastPanelData = {
149   - notification: notificationMessage
  134 + notification: notificationMessage,
  135 + panelClass,
  136 + destroyToastComponent: () => {
  137 + this.viewContainerRef.detach(0);
  138 + this.toastComponentRef.destroy();
  139 + }
150 140 };
151 141 const injectionTokens = new WeakMap<any, any>([
152   - [MAT_SNACK_BAR_DATA, data],
153   - [OverlayRef, this.overlayRef]
  142 + [MAT_SNACK_BAR_DATA, data]
154 143 ]);
155 144 const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
156   - this.toastComponentRef = this.overlayRef.attach(new ComponentPortal(TbSnackBarComponent, this.viewContainerRef, injector));
  145 + this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector);
157 146 this.cd.detectChanges();
158 147
159 148 if (notificationMessage.duration && notificationMessage.duration > 0) {
... ... @@ -173,7 +162,6 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
173 162 clearTimeout(this.dismissTimeout);
174 163 this.dismissTimeout = null;
175 164 }
176   - this.overlayRef = null;
177 165 this.toastComponentRef = null;
178 166 this.currentMessage = null;
179 167 });
... ... @@ -181,43 +169,45 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
181 169 }
182 170
183 171 private showSnackBar(notificationMessage: NotificationMessage, isGtSm: boolean) {
184   - const data: ToastPanelData = {
185   - notification: notificationMessage,
186   - parent: this.elementRef
187   - };
188   - const config: MatSnackBarConfig = {
189   - horizontalPosition: notificationMessage.horizontalPosition || 'left',
190   - verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'),
191   - viewContainerRef: this.viewContainerRef,
192   - duration: notificationMessage.duration,
193   - panelClass: notificationMessage.panelClass,
194   - data
195   - };
196 172 this.ngZone.run(() => {
197 173 if (this.snackBarRef) {
198 174 this.snackBarRef.dismiss();
199 175 }
  176 + const data: ToastPanelData = {
  177 + notification: notificationMessage,
  178 + parent: this.elementRef,
  179 + panelClass: [],
  180 + destroyToastComponent: () => {}
  181 + };
  182 + const config: MatSnackBarConfig = {
  183 + horizontalPosition: notificationMessage.horizontalPosition || 'left',
  184 + verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'),
  185 + viewContainerRef: this.viewContainerRef,
  186 + duration: notificationMessage.duration,
  187 + panelClass: notificationMessage.panelClass,
  188 + data
  189 + };
200 190 this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config);
201   - });
202   - if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) {
203   - if (this.dismissTimeout !== null) {
204   - clearTimeout(this.dismissTimeout);
205   - this.dismissTimeout = null;
206   - }
207   - this.dismissTimeout = setTimeout(() => {
208   - if (this.snackBarRef) {
209   - this.snackBarRef.instance.actionButton._elementRef.nativeElement.click();
  191 + if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) {
  192 + if (this.dismissTimeout !== null) {
  193 + clearTimeout(this.dismissTimeout);
  194 + this.dismissTimeout = null;
210 195 }
211   - this.dismissTimeout = null;
212   - }, notificationMessage.duration);
213   - }
214   - this.snackBarRef.afterDismissed().subscribe(() => {
215   - if (this.dismissTimeout !== null) {
216   - clearTimeout(this.dismissTimeout);
217   - this.dismissTimeout = null;
  196 + this.dismissTimeout = setTimeout(() => {
  197 + if (this.snackBarRef) {
  198 + this.snackBarRef.instance.actionButton._elementRef.nativeElement.click();
  199 + }
  200 + this.dismissTimeout = null;
  201 + }, notificationMessage.duration);
218 202 }
219   - this.snackBarRef = null;
220   - this.currentMessage = null;
  203 + this.snackBarRef.afterDismissed().subscribe(() => {
  204 + if (this.dismissTimeout !== null) {
  205 + clearTimeout(this.dismissTimeout);
  206 + this.dismissTimeout = null;
  207 + }
  208 + this.snackBarRef = null;
  209 + this.currentMessage = null;
  210 + });
221 211 });
222 212 }
223 213
... ... @@ -235,8 +225,9 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
235 225 }
236 226
237 227 ngOnDestroy(): void {
238   - if (this.overlayRef) {
239   - this.overlayRef.dispose();
  228 + if (this.toastComponentRef) {
  229 + this.viewContainerRef.detach(0);
  230 + this.toastComponentRef.destroy();
240 231 }
241 232 if (this.notificationSubscription) {
242 233 this.notificationSubscription.unsubscribe();
... ... @@ -250,6 +241,8 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
250 241 interface ToastPanelData {
251 242 notification: NotificationMessage;
252 243 parent?: ElementRef;
  244 + panelClass: string[];
  245 + destroyToastComponent: () => void;
253 246 }
254 247
255 248 import {
... ... @@ -288,6 +281,11 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
288 281
289 282 @ViewChild('actionButton', {static: true}) actionButton: MatButton;
290 283
  284 + @HostBinding('class')
  285 + get panelClass(): string[] {
  286 + return this.data.panelClass;
  287 + }
  288 +
291 289 private parentEl: HTMLElement;
292 290 private snackBarContainerEl: HTMLElement;
293 291 private parentScrollSubscription: Subscription = null;
... ... @@ -305,9 +303,7 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
305 303 private data: ToastPanelData,
306 304 private elementRef: ElementRef,
307 305 @Optional()
308   - private snackBarRef: MatSnackBarRef<TbSnackBarComponent>,
309   - @Optional()
310   - private overlayRef: OverlayRef) {
  306 + private snackBarRef: MatSnackBarRef<TbSnackBarComponent>) {
311 307 this.animationState = !!this.snackBarRef ? 'default' : 'opened';
312 308 this.notification = data.notification;
313 309 }
... ... @@ -319,9 +315,8 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
319 315 this.snackBarContainerEl.style.position = 'absolute';
320 316 this.updateContainerRect();
321 317 this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig);
322   - const snackBarComponent = this;
323 318 this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => {
324   - snackBarComponent.updateContainerRect();
  319 + this.updateContainerRect();
325 320 });
326 321 }
327 322 }
... ... @@ -373,7 +368,7 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
373 368 const isFadeOut = (toState as ToastAnimationState) === 'closing';
374 369 const itFinished = this.animationState === 'closing';
375 370 if (isFadeOut && itFinished) {
376   - this.overlayRef.dispose();
  371 + this.data.destroyToastComponent();
377 372 }
378 373 }
379 374 }
... ...