mirror of https://github.com/abpframework/abp.git
28 changed files with 727 additions and 367 deletions
@ -1,56 +0,0 @@ |
|||
import { MessageService } from 'primeng/components/common/messageservice'; |
|||
import { Observable, Subject } from 'rxjs'; |
|||
import { Toaster } from '../models/toaster'; |
|||
import { Config } from '@abp/ng.core'; |
|||
|
|||
export abstract class AbstractToaster<T = Toaster.Options> { |
|||
status$: Subject<Toaster.Status>; |
|||
|
|||
key = 'abpToast'; |
|||
|
|||
sticky = false; |
|||
|
|||
constructor(protected messageService: MessageService) {} |
|||
|
|||
info(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> { |
|||
return this.show(message, title, 'info', options); |
|||
} |
|||
|
|||
success(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> { |
|||
return this.show(message, title, 'success', options); |
|||
} |
|||
|
|||
warn(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> { |
|||
return this.show(message, title, 'warn', options); |
|||
} |
|||
|
|||
error(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> { |
|||
return this.show(message, title, 'error', options); |
|||
} |
|||
|
|||
protected show( |
|||
message: Config.LocalizationParam, |
|||
title: Config.LocalizationParam, |
|||
severity: Toaster.Severity, |
|||
options?: T, |
|||
): Observable<Toaster.Status> { |
|||
this.messageService.clear(this.key); |
|||
|
|||
this.messageService.add({ |
|||
severity, |
|||
detail: message || '', |
|||
summary: title || '', |
|||
...options, |
|||
key: this.key, |
|||
...(typeof (options || ({} as any)).sticky === 'undefined' && { sticky: this.sticky }), |
|||
}); |
|||
this.status$ = new Subject<Toaster.Status>(); |
|||
return this.status$; |
|||
} |
|||
|
|||
clear(status?: Toaster.Status) { |
|||
this.messageService.clear(this.key); |
|||
this.status$.next(status || Toaster.Status.dismiss); |
|||
this.status$.complete(); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
import { animate, query, style, transition, trigger } from '@angular/animations'; |
|||
|
|||
export const toastInOut = trigger('toastInOut', [ |
|||
transition('* <=> *', [ |
|||
query( |
|||
':enter', |
|||
[ |
|||
style({ opacity: 0, transform: 'translateY(20px)' }), |
|||
animate('350ms ease', style({ opacity: 1, transform: 'translateY(0)' })), |
|||
], |
|||
{ optional: true }, |
|||
), |
|||
query(':leave', animate('450ms ease', style({ opacity: 0 })), { |
|||
optional: true, |
|||
}), |
|||
]), |
|||
]); |
|||
@ -0,0 +1,34 @@ |
|||
<div class="confirmation show" *ngIf="visible"> |
|||
<div class="confirmation-backdrop"></div> |
|||
<div class="confirmation-dialog"> |
|||
<div class="icon-container" [ngClass]="data.severity" *ngIf="data.severity"> |
|||
<i class="fa icon" [ngClass]="iconClass"></i> |
|||
</div> |
|||
<div class="content"> |
|||
<h1 class="title" *ngIf="data.title"> |
|||
{{ data.title | abpLocalization: data.options?.titleLocalizationParams }} |
|||
</h1> |
|||
<p class="message" *ngIf="data.message"> |
|||
{{ data.message | abpLocalization: data.options?.messageLocalizationParams }} |
|||
</p> |
|||
</div> |
|||
<div class="footer"> |
|||
<button |
|||
id="cancel" |
|||
class="confirmation-button confirmation-button-reject" |
|||
*ngIf="!data?.options?.hideCancelBtn" |
|||
(click)="close(reject)" |
|||
> |
|||
{{ data.options?.cancelText || 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
<button |
|||
id="confirm" |
|||
class="confirmation-button confirmation-button-approve" |
|||
*ngIf="!data?.options?.hideYesBtn" |
|||
(click)="close(confirm)" |
|||
> |
|||
{{ data.options?.yesText || 'AbpUi::Yes' | abpLocalization }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,120 @@ |
|||
.confirmation { |
|||
position: fixed; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
display: none; |
|||
place-items: center; |
|||
z-index: 1060; |
|||
&.show { |
|||
display: grid; |
|||
} |
|||
.confirmation-backdrop { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-color: rgba(#000, 0.7); |
|||
z-index: 1061 !important; |
|||
} |
|||
.confirmation-dialog { |
|||
display: flex; |
|||
flex-direction: column; |
|||
margin: 20px auto; |
|||
padding: 0; |
|||
border: none; |
|||
border-radius: 10px; |
|||
min-width: 450px; |
|||
min-height: 300px; |
|||
background-color: #fff; |
|||
box-shadow: 0 0 10px -5px rgba(#000, 0.5); |
|||
z-index: 1062 !important; |
|||
.icon-container { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin: 0 0 10px 0; |
|||
padding: 20px; |
|||
.icon { |
|||
width: 100px; |
|||
height: 100px; |
|||
stroke-width: 1; |
|||
fill: #fff; |
|||
font-size: 80px; |
|||
text-align: center; |
|||
} |
|||
&.neutral .icon { |
|||
} |
|||
&.info .icon { |
|||
stroke: #2f96b4; |
|||
color: #2f96b4; |
|||
} |
|||
&.success .icon { |
|||
stroke: #51a351; |
|||
color: #51a351; |
|||
} |
|||
&.warning .icon { |
|||
stroke: #f89406; |
|||
color: #f89406; |
|||
} |
|||
&.error .icon { |
|||
stroke: #bd362f; |
|||
color: #bd362f; |
|||
} |
|||
} |
|||
.content { |
|||
flex-grow: 1; |
|||
display: block; |
|||
.title { |
|||
display: block; |
|||
margin: 0; |
|||
padding: 0; |
|||
font-size: 27px; |
|||
font-weight: 600; |
|||
text-align: center; |
|||
} |
|||
.message { |
|||
display: block; |
|||
margin: 10px auto; |
|||
padding: 0; |
|||
color: #777; |
|||
font-size: 16px; |
|||
font-weight: 400; |
|||
text-align: center; |
|||
} |
|||
} |
|||
.footer { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
margin: 10px 0 0 0; |
|||
padding: 20px; |
|||
width: 100%; |
|||
.confirmation-button { |
|||
display: inline-block; |
|||
margin: 0px 5px; |
|||
padding: 10px 20px; |
|||
border: none; |
|||
border-radius: 6px; |
|||
color: #777; |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
background-color: #eee; |
|||
&:hover { |
|||
background-color: darken(#eee, 5); |
|||
} |
|||
&-reject { |
|||
} |
|||
&-approve { |
|||
background-color: #2f96b4; |
|||
color: #fff; |
|||
&:hover { |
|||
background-color: darken(#2f96b4, 5); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
.modal { |
|||
&.show { |
|||
display: block !important; |
|||
} |
|||
|
|||
&-backdrop { |
|||
background-color: rgba(0, 0, 0, 0.6); |
|||
} |
|||
|
|||
&::-webkit-scrollbar { |
|||
width: 7px; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-track { |
|||
background: #ddd; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-thumb { |
|||
background: #8a8686; |
|||
} |
|||
|
|||
&-dialog { |
|||
z-index: 1050; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<div |
|||
class="toast-container" |
|||
[style.top]="top || 'auto'" |
|||
[style.right]="right || 'auto'" |
|||
[style.bottom]="bottom || 'auto'" |
|||
[style.left]="left || 'auto'" |
|||
[style.display]="toasts.length ? 'flex' : 'none'" |
|||
[@toastInOut]="toasts.length - 1 || 0" |
|||
> |
|||
<abp-toast [toast]="toast" *ngFor="let toast of toasts; trackBy: trackByFunc"></abp-toast> |
|||
</div> |
|||
@ -0,0 +1,12 @@ |
|||
.toast-container { |
|||
position: fixed; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
min-width: 350px; |
|||
min-height: 80px; |
|||
&.new-on-top { |
|||
flex-direction: column-reverse; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
import { Component, Input, OnInit } from '@angular/core'; |
|||
import { Toaster } from '../../models'; |
|||
import { toastInOut } from '../../animations'; |
|||
import { ToasterService } from '../../services/toaster.service'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-toast-container', |
|||
templateUrl: './toast-container.component.html', |
|||
styleUrls: ['./toast-container.component.scss'], |
|||
animations: [toastInOut], |
|||
}) |
|||
export class ToastContainerComponent implements OnInit { |
|||
toasts = [] as Toaster.Toast[]; |
|||
|
|||
@Input() |
|||
top: number; |
|||
|
|||
@Input() |
|||
right: number; |
|||
|
|||
@Input() |
|||
bottom: number; |
|||
|
|||
@Input() |
|||
left: number; |
|||
|
|||
@Input() |
|||
toastKey: string; |
|||
|
|||
constructor(private toastService: ToasterService) {} |
|||
|
|||
ngOnInit() { |
|||
this.toastService.toasts$.subscribe(toasts => { |
|||
this.toasts = this.toastKey |
|||
? toasts.filter(t => { |
|||
return t.options && t.options.containerKey !== this.toastKey; |
|||
}) |
|||
: toasts; |
|||
}); |
|||
} |
|||
|
|||
trackByFunc(index, toast) { |
|||
if (!toast) return null; |
|||
return toast.options.id; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<div class="toast" [ngClass]="severityClass" (click)="tap()"> |
|||
<div class="toast-icon"> |
|||
<i class="fa icon" [ngClass]="iconClass"></i> |
|||
</div> |
|||
<div class="toast-content"> |
|||
<button class="close-button" (click)="close()" *ngIf="toast.options.closable"> |
|||
<i class="fa fa-times"></i> |
|||
</button> |
|||
<div class="toast-title"> |
|||
{{ toast.title | abpLocalization: toast.options?.titleLocalizationParams }} |
|||
</div> |
|||
<div class="toast-message"> |
|||
{{ toast.message | abpLocalization: toast.options?.messageLocalizationParams }} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,80 @@ |
|||
@mixin fillColor($background, $color) { |
|||
border: 2px solid $background; |
|||
background-color: $background; |
|||
color: $color; |
|||
box-shadow: 0 0 10px -5px rgba(#000, 0.4); |
|||
&:hover { |
|||
border: 2px solid darken($background, 5); |
|||
background-color: darken($background, 5); |
|||
box-shadow: 0 0 15px -5px rgba(#000, 0.4); |
|||
} |
|||
} |
|||
|
|||
.toast { |
|||
display: grid; |
|||
grid-template-columns: 50px 1fr; |
|||
gap: 10px; |
|||
margin: 5px 0; |
|||
padding: 10px; |
|||
border-radius: 0px; |
|||
width: 350px; |
|||
user-select: none; |
|||
box-shadow: 0 0 10px -5px rgba(#000, 0.4); |
|||
z-index: 9999; |
|||
@include fillColor(#f0f0f0, #000); |
|||
opacity: 1; |
|||
&.toast-success { |
|||
@include fillColor(#51a351, #fff); |
|||
} |
|||
&.toast-info { |
|||
@include fillColor(#2f96b4, #fff); |
|||
} |
|||
&.toast-warning { |
|||
@include fillColor(#f89406, #fff); |
|||
} |
|||
&.toast-error { |
|||
@include fillColor(#bd362f, #fff); |
|||
} |
|||
.toast-icon { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
.icon { |
|||
font-size: 36px; |
|||
} |
|||
} |
|||
.toast-content { |
|||
position: relative; |
|||
.close-button { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin: 0; |
|||
padding: 5px 10px 5px 5px; |
|||
width: 25px; |
|||
height: 25px; |
|||
border: none; |
|||
border-radius: 50%; |
|||
background: transparent; |
|||
&:focus { |
|||
outline: none; |
|||
} |
|||
.close-icon { |
|||
width: 16px; |
|||
height: 16px; |
|||
stroke: #000; |
|||
} |
|||
} |
|||
.toast-title { |
|||
margin: 0; |
|||
padding: 0; |
|||
font-size: 1rem; |
|||
font-weight: 600; |
|||
} |
|||
.toast-message { |
|||
} |
|||
} |
|||
} |
|||
@ -1,26 +1,56 @@ |
|||
import { Component } from '@angular/core'; |
|||
import { Component, Input, OnInit } from '@angular/core'; |
|||
import { Toaster } from '../../models'; |
|||
import { ToasterService } from '../../services/toaster.service'; |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
import snq from 'snq'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-toast', |
|||
// tslint:disable-next-line: component-max-inline-declarations
|
|||
template: ` |
|||
<p-toast position="bottom-right" key="abpToast" styleClass="abp-toast" [baseZIndex]="1000"> |
|||
<ng-template let-message pTemplate="message"> |
|||
<span |
|||
class="ui-toast-icon pi" |
|||
[ngClass]="{ |
|||
'pi-info-circle': message.severity === 'info', |
|||
'pi-exclamation-triangle': message.severity === 'warn', |
|||
'pi-times': message.severity === 'error', |
|||
'pi-check': message.severity === 'success' |
|||
}" |
|||
></span> |
|||
<div class="ui-toast-message-text-content"> |
|||
<div class="ui-toast-summary">{{ message.summary | abpLocalization: message.titleLocalizationParams }}</div> |
|||
<div class="ui-toast-detail">{{ message.detail | abpLocalization: message.messageLocalizationParams }}</div> |
|||
</div> |
|||
</ng-template> |
|||
</p-toast> |
|||
`,
|
|||
templateUrl: './toast.component.html', |
|||
styleUrls: ['./toast.component.scss'], |
|||
}) |
|||
export class ToastComponent {} |
|||
export class ToastComponent implements OnInit { |
|||
@Input() |
|||
toast: Toaster.Toast; |
|||
|
|||
get severityClass(): string { |
|||
if (!this.toast || !this.toast.severity) return ''; |
|||
return `toast-${this.toast.severity}`; |
|||
} |
|||
|
|||
get iconClass(): string { |
|||
switch (this.toast.severity) { |
|||
case 'success': |
|||
return 'fa-check-circle'; |
|||
case 'info': |
|||
return 'fa-info-circle'; |
|||
case 'warning': |
|||
return 'fa-exclamation-triangle'; |
|||
case 'error': |
|||
return 'fa-times-circle'; |
|||
default: |
|||
return 'fa-exclamation-circle'; |
|||
} |
|||
} |
|||
|
|||
constructor( |
|||
private toastService: ToasterService, |
|||
private localizationService: LocalizationService, |
|||
) {} |
|||
|
|||
ngOnInit() { |
|||
if (snq(() => this.toast.options.sticky)) return; |
|||
const timeout = snq(() => this.toast.options.life) || 5000; |
|||
setTimeout(() => { |
|||
this.close(); |
|||
}, timeout); |
|||
} |
|||
|
|||
close() { |
|||
this.toastService.remove(this.toast.options.id); |
|||
} |
|||
|
|||
tap() { |
|||
if (this.toast.options && this.toast.options.tapToDismiss) this.close(); |
|||
} |
|||
} |
|||
|
|||
@ -1,11 +1,23 @@ |
|||
import { Toaster } from './toaster'; |
|||
import { Config } from '@abp/ng.core'; |
|||
|
|||
export namespace Confirmation { |
|||
export interface Options extends Toaster.Options { |
|||
export interface Options { |
|||
id?: any; |
|||
closable?: boolean; |
|||
messageLocalizationParams?: string[]; |
|||
titleLocalizationParams?: string[]; |
|||
hideCancelBtn?: boolean; |
|||
hideYesBtn?: boolean; |
|||
cancelText?: Config.LocalizationParam; |
|||
yesText?: Config.LocalizationParam; |
|||
} |
|||
|
|||
export interface DialogData { |
|||
message: Config.LocalizationParam; |
|||
title?: Config.LocalizationParam; |
|||
severity?: Severity; |
|||
options?: Partial<Options>; |
|||
} |
|||
|
|||
export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error'; |
|||
} |
|||
|
|||
@ -1,15 +1,116 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { AbstractToaster } from '../abstracts/toaster'; |
|||
import { Message } from 'primeng/components/common/message'; |
|||
import { MessageService } from 'primeng/components/common/messageservice'; |
|||
import { Toaster } from '../models'; |
|||
import { ReplaySubject } from 'rxjs'; |
|||
import { Config } from '@abp/ng.core'; |
|||
import snq from 'snq'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class ToasterService extends AbstractToaster { |
|||
constructor(protected messageService: MessageService) { |
|||
super(messageService); |
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class ToasterService { |
|||
toasts$ = new ReplaySubject<Toaster.Toast[]>(1); |
|||
|
|||
private lastId = -1; |
|||
|
|||
private toasts = [] as Toaster.Toast[]; |
|||
|
|||
/** |
|||
* Creates an info toast with given parameters. |
|||
* @param message Content of the toast |
|||
* @param title Title of the toast |
|||
* @param options Spesific style or structural options for individual toast |
|||
*/ |
|||
info( |
|||
message: Config.LocalizationParam, |
|||
title?: Config.LocalizationParam, |
|||
options?: Partial<Toaster.ToastOptions>, |
|||
) { |
|||
return this.show(message, title, 'info', options); |
|||
} |
|||
|
|||
/** |
|||
* Creates a success toast with given parameters. |
|||
* @param message Content of the toast |
|||
* @param title Title of the toast |
|||
* @param options Spesific style or structural options for individual toast |
|||
*/ |
|||
success( |
|||
message: Config.LocalizationParam, |
|||
title?: Config.LocalizationParam, |
|||
options?: Partial<Toaster.ToastOptions>, |
|||
) { |
|||
return this.show(message, title, 'success', options); |
|||
} |
|||
|
|||
/** |
|||
* Creates a warning toast with given parameters. |
|||
* @param message Content of the toast |
|||
* @param title Title of the toast |
|||
* @param options Spesific style or structural options for individual toast |
|||
*/ |
|||
warn( |
|||
message: Config.LocalizationParam, |
|||
title?: Config.LocalizationParam, |
|||
options?: Partial<Toaster.ToastOptions>, |
|||
) { |
|||
return this.show(message, title, 'warning', options); |
|||
} |
|||
|
|||
/** |
|||
* Creates an error toast with given parameters. |
|||
* @param message Content of the toast |
|||
* @param title Title of the toast |
|||
* @param options Spesific style or structural options for individual toast |
|||
*/ |
|||
error( |
|||
message: Config.LocalizationParam, |
|||
title?: Config.LocalizationParam, |
|||
options?: Partial<Toaster.ToastOptions>, |
|||
) { |
|||
return this.show(message, title, 'error', options); |
|||
} |
|||
|
|||
/** |
|||
* Creates a toast with given parameters. |
|||
* @param message Content of the toast |
|||
* @param title Title of the toast |
|||
* @param severity Sets color of the toast. "success", "warning" etc. |
|||
* @param options Spesific style or structural options for individual toast |
|||
*/ |
|||
|
|||
show( |
|||
message: Config.LocalizationParam, |
|||
title: Config.LocalizationParam = null, |
|||
severity: Toaster.Severity = 'neutral', |
|||
options = {} as Partial<Toaster.ToastOptions>, |
|||
) { |
|||
const id = ++this.lastId; |
|||
this.toasts.push({ |
|||
message, |
|||
title, |
|||
severity, |
|||
options: { closable: true, id, ...options }, |
|||
}); |
|||
this.toasts$.next(this.toasts); |
|||
return id; |
|||
} |
|||
|
|||
/** |
|||
* Removes the toast with given id. |
|||
* @param id ID of the toast to be removed. |
|||
*/ |
|||
remove(id: number) { |
|||
this.toasts = this.toasts.filter(toast => snq(() => toast.options.id) !== id); |
|||
this.toasts$.next(this.toasts); |
|||
} |
|||
|
|||
addAll(messages: Message[]): void { |
|||
this.messageService.addAll(messages.map(message => ({ key: this.key, ...message }))); |
|||
/** |
|||
* Removes all open toasts at once. |
|||
*/ |
|||
clear(key?: string) { |
|||
this.toasts = !key |
|||
? [] |
|||
: this.toasts.filter(toast => snq(() => toast.options.containerKey) !== key); |
|||
this.toasts$.next(this.toasts); |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue