diff --git a/src/app/+home-page/home-page.component.html b/src/app/+home-page/home-page.component.html index 6a3e20ca9d..969c41e779 100644 --- a/src/app/+home-page/home-page.component.html +++ b/src/app/+home-page/home-page.component.html @@ -3,3 +3,17 @@ + + + + + + + + + + + +

Simple example

+ +
diff --git a/src/app/+home-page/home-page.component.ts b/src/app/+home-page/home-page.component.ts index ad25ec0155..7346417e16 100644 --- a/src/app/+home-page/home-page.component.ts +++ b/src/app/+home-page/home-page.component.ts @@ -1,4 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { Options } from '../shared/notifications/interfaces/options.type'; @Component({ selector: 'ds-home-page', @@ -6,5 +8,39 @@ import { Component } from '@angular/core'; templateUrl: './home-page.component.html' }) export class HomePageComponent { + public notificationOptions: Options = { + position: ['top', 'right'], + timeOut: 0, + lastOnBottom: true, + clickIconToClose: false, + showProgressBar: true, + }; + @ViewChild('example') example: TemplateRef; + + constructor(private notificationsService: NotificationsService) { + } + + createNotification() { + const n1 = this.notificationsService.success('Welcome in DSpace', 'Good choice!', + { + showProgressBar: false, + animate: 'rotate', + timeout: 2000}); + const n2 = this.notificationsService.error('Error in DSpace', 'This is a fake error!'); + const n3 = this.notificationsService.info(this.example); + console.log('Notifications pushed'); + console.log(n1); + console.log(n2); + } + + notificationCreated(event) { + console.log('Notification created'); + console.log(event); + } + + notificationDestroyed() { + console.log('Notification destroyed'); + console.log(event); + } } diff --git a/src/app/+home-page/home-page.module.ts b/src/app/+home-page/home-page.module.ts index 0a513260cd..41d733dc6d 100644 --- a/src/app/+home-page/home-page.module.ts +++ b/src/app/+home-page/home-page.module.ts @@ -6,6 +6,8 @@ import { HomePageRoutingModule } from './home-page-routing.module'; import { HomePageComponent } from './home-page.component'; import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component'; +import { NotificationComponent } from '../shared/notifications/notification/notification.component'; +import { NotificationsBoardComponent } from '../shared/notifications/notifications-board/notifications-board.component'; @NgModule({ imports: [ @@ -16,7 +18,9 @@ import { TopLevelCommunityListComponent } from './top-level-community-list/top-l declarations: [ HomePageComponent, TopLevelCommunityListComponent, - HomeNewsComponent + HomeNewsComponent, + // NotificationComponent, + // NotificationsBoardComponent ] }) export class HomePageModule { diff --git a/src/app/app.component.html b/src/app/app.component.html index fd1ad55d44..b5922e395f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,11 +1,21 @@ -
-
- - -
- -
- - -
-
+
+
+ + +

Inizio notifiche

+ + +

Fine notifiche

+ +
+ +
+ + +
+
+ + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 25bdde2d23..5069e04892 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -17,6 +17,7 @@ import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/host-window.reducer'; import { NativeWindowRef, NativeWindowService } from './shared/window.service'; +import { Options } from './shared/notifications/interfaces/options.type'; @Component({ selector: 'ds-app', @@ -27,6 +28,24 @@ import { NativeWindowRef, NativeWindowService } from './shared/window.service'; }) export class AppComponent implements OnInit { + public notificationOptions: Options = { + position: ['top', 'right'], + timeOut: 5000, + lastOnBottom: true, + clickIconToClose: true, + showProgressBar: true + }; + + notificationCreated(event) { + console.log('Notification created'); + console.log(event); + } + + notificationDestroyed() { + console.log('Notification destroyed'); + console.log(event); + } + constructor( @Inject(GLOBAL_CONFIG) public config: GlobalConfig, @Inject(NativeWindowService) private _window: NativeWindowRef, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d2b0d72b78..12f003cab7 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,6 @@ import { APP_BASE_HREF, CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; -import { BrowserTransferStateModule } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @@ -29,6 +28,9 @@ import { HeaderComponent } from './header/header.component'; import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer'; +import { NotificationsService } from './shared/notifications/notifications.service'; +import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component'; +import { NotificationComponent } from './shared/notifications/notification/notification.component'; export function getConfig() { return ENV_CONFIG; @@ -85,7 +87,9 @@ if (!ENV_CONFIG.production) { AppComponent, HeaderComponent, FooterComponent, - PageNotFoundComponent + PageNotFoundComponent, + NotificationComponent, + NotificationsBoardComponent ], exports: [AppComponent] }) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 768f05f24b..41cc0b88ce 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -38,6 +38,7 @@ import { SubmissionDefinitionsConfigService } from './config/submission-definiti import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; import { UUIDService } from './shared/uuid.service'; +import { NotificationsService } from '../shared/notifications/notifications.service'; const IMPORTS = [ CommonModule, @@ -77,6 +78,7 @@ const PROVIDERS = [ SubmissionFormsConfigService, SubmissionSectionsConfigService, UUIDService, + NotificationsService, { provide: NativeWindowService, useFactory: NativeWindowFactory } ]; diff --git a/src/app/shared/notifications/interfaces/icons.ts b/src/app/shared/notifications/interfaces/icons.ts new file mode 100644 index 0000000000..bf3066c2ac --- /dev/null +++ b/src/app/shared/notifications/interfaces/icons.ts @@ -0,0 +1,41 @@ +export interface Icons { + alert: string; + error: string; + info: string; + warn: string; + success: string; +} + +export const defaultIcons: Icons = { + alert: ` + + + + + `, + error: ` + + + + + `, + info: ` + + + + + `, + success: ` + + + + + `, + warn: ` + + + + + + ` +}; diff --git a/src/app/shared/notifications/interfaces/notification-event.type.ts b/src/app/shared/notifications/interfaces/notification-event.type.ts new file mode 100644 index 0000000000..978cf3dc69 --- /dev/null +++ b/src/app/shared/notifications/interfaces/notification-event.type.ts @@ -0,0 +1,8 @@ +import {Notification} from './notification.type'; + +export interface NotificationEvent { + add?: boolean; + command: string; + id?: string; + notification?: Notification; +} diff --git a/src/app/shared/notifications/interfaces/notification.type.ts b/src/app/shared/notifications/interfaces/notification.type.ts new file mode 100644 index 0000000000..a6cdc890da --- /dev/null +++ b/src/app/shared/notifications/interfaces/notification.type.ts @@ -0,0 +1,24 @@ +import {EventEmitter} from '@angular/core'; + +export interface Notification { + id?: string + type: string + icon: string + title?: any + content?: any + override?: any + html?: any + state?: string + createdOn?: Date + destroyedOn?: Date + animate?: string + timeOut?: number + maxLength?: number + pauseOnHover?: boolean + clickToClose?: boolean + clickIconToClose?: boolean + theClass?: string + click?: EventEmitter<{}>; + clickIcon?: EventEmitter<{}>; + timeoutEnd?: EventEmitter<{}>; +} diff --git a/src/app/shared/notifications/interfaces/options.type.ts b/src/app/shared/notifications/interfaces/options.type.ts new file mode 100644 index 0000000000..46c4eb9b37 --- /dev/null +++ b/src/app/shared/notifications/interfaces/options.type.ts @@ -0,0 +1,19 @@ +import {Icons} from './icons'; + +export interface Options { + timeOut?: number; + showProgressBar?: boolean; + pauseOnHover?: boolean; + lastOnBottom?: boolean; + clickToClose?: boolean; + clickIconToClose?: boolean; + maxLength?: number; + maxStack?: number; + preventDuplicates?: boolean; + preventLastDuplicates?: boolean | string; + theClass?: string; + rtl?: boolean; + animate?: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale'; + icons?: Icons; + position?: ['top' | 'bottom' | 'middle', 'right' | 'left' | 'center']; +} diff --git a/src/app/shared/notifications/notification/notification.component.html b/src/app/shared/notifications/notification/notification.component.html new file mode 100644 index 0000000000..b77f3657a4 --- /dev/null +++ b/src/app/shared/notifications/notification/notification.component.html @@ -0,0 +1,54 @@ +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+
+
+
+ +
+ + +
+
+ +
+
+ +
+ +
+ +
diff --git a/src/app/shared/notifications/notification/notification.component.scss b/src/app/shared/notifications/notification/notification.component.scss new file mode 100644 index 0000000000..22ad8c0e2d --- /dev/null +++ b/src/app/shared/notifications/notification/notification.component.scss @@ -0,0 +1,107 @@ +.simple-notification { + width: 100%; + padding: 10px 20px; + box-sizing: border-box; + position: relative; + float: left; + margin-bottom: 10px; + color: #fff; + cursor: pointer; + transition: all 0.5s; + min-height: 70px; +} + +.simple-notification .sn-title, +.simple-notification .sn-content, +.simple-notification .sn-html { + margin: 0; +} + +.simple-notification .sn-title { + line-height: 30px; + font-size: 20px; +} + +.simple-notification .sn-content { + font-size: 16px; + line-height: 20px; +} + +.simple-notification.has-icon .sn-title, +.simple-notification.has-icon .sn-content, +.simple-notification.has-icon .sn-html { + padding: 0 50px 0 0; +} + +.simple-notification .icon { + position: absolute; + box-sizing: border-box; + top: 0; + right: 0; + width: 70px; + height: 70px; + padding: 10px; +} + +.simple-notification .icon.icon-hover:hover { + opacity: 0.5; +} + +.simple-notification .icon svg { + fill: #fff; + width: 100%; + height: 100%; +} + +.simple-notification .icon svg g { + fill: #fff; +} + +.simple-notification.rtl-mode.has-icon .sn-title, +.simple-notification.rtl-mode.has-icon .sn-content, +.simple-notification.rtl-mode.has-icon .sn-html { + padding: 0 0 0 50px; +} + +.simple-notification.rtl-mode { + direction: rtl; +} + +.simple-notification.rtl-mode .sn-content { + padding: 0 0 0 50px; +} + +.simple-notification.rtl-mode svg { + left: 0; + right: auto; +} + +.simple-notification.error { background: #F44336; } +.simple-notification.success { background: #8BC34A; } +.simple-notification.alert { background: #ffdb5b; } +.simple-notification.info { background: #03A9F4; } +.simple-notification.warn { background: #ffdb5b; } + +.simple-notification .sn-progress-loader { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 5px; +} + +.simple-notification .sn-progress-loader span { + float: left; + height: 100%; +} + +.simple-notification.success .sn-progress-loader span { background: #689F38; } +.simple-notification.error .sn-progress-loader span { background: #D32F2F; } +.simple-notification.alert .sn-progress-loader span { background: #edc242; } +.simple-notification.info .sn-progress-loader span { background: #0288D1; } +.simple-notification.warn .sn-progress-loader span { background: #edc242; } +.simple-notification.bare .sn-progress-loader span { background: #ccc; } + +.simple-notification.warn div .sn-title, +.simple-notification.warn div .sn-content, +.simple-notification.warn div .sn-html { color: #444; } diff --git a/src/app/shared/notifications/notification/notification.component.ts b/src/app/shared/notifications/notification/notification.component.ts new file mode 100644 index 0000000000..461ca2621f --- /dev/null +++ b/src/app/shared/notifications/notification/notification.component.ts @@ -0,0 +1,259 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + NgZone, + OnDestroy, + OnInit, + TemplateRef, + ViewEncapsulation +} from '@angular/core'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { Notification } from '../interfaces/notification.type'; +import { NotificationsService } from '../notifications.service'; + +@Component({ + selector: 'ds-notification', + encapsulation: ViewEncapsulation.None, + animations: [ + trigger('enterLeave', [ + + // Fade + state('fade', style({opacity: 1})), + transition('* => fade', [ + style({opacity: 0}), + animate('400ms ease-in-out') + ]), + state('fadeOut', style({opacity: 0})), + transition('fade => fadeOut', [ + style({opacity: 1}), + animate('300ms ease-in-out') + ]), + + // Enter from top + state('fromTop', style({opacity: 1, transform: 'translateY(0)'})), + transition('* => fromTop', [ + style({opacity: 0, transform: 'translateY(-5%)'}), + animate('400ms ease-in-out') + ]), + state('fromTopOut', style({opacity: 0, transform: 'translateY(5%)'})), + transition('fromTop => fromTopOut', [ + style({opacity: 1, transform: 'translateY(0)'}), + animate('300ms ease-in-out') + ]), + + // Enter from right + state('fromRight', style({opacity: 1, transform: 'translateX(0)'})), + transition('* => fromRight', [ + style({opacity: 0, transform: 'translateX(5%)'}), + animate('400ms ease-in-out') + ]), + state('fromRightOut', style({opacity: 0, transform: 'translateX(-5%)'})), + transition('fromRight => fromRightOut', [ + style({opacity: 1, transform: 'translateX(0)'}), + animate('300ms ease-in-out') + ]), + + // Enter from bottom + state('fromBottom', style({opacity: 1, transform: 'translateY(0)'})), + transition('* => fromBottom', [ + style({opacity: 0, transform: 'translateY(5%)'}), + animate('400ms ease-in-out') + ]), + state('fromBottomOut', style({opacity: 0, transform: 'translateY(-5%)'})), + transition('fromBottom => fromBottomOut', [ + style({opacity: 1, transform: 'translateY(0)'}), + animate('300ms ease-in-out') + ]), + + // Enter from left + state('fromLeft', style({opacity: 1, transform: 'translateX(0)'})), + transition('* => fromLeft', [ + style({opacity: 0, transform: 'translateX(-5%)'}), + animate('400ms ease-in-out') + ]), + state('fromLeftOut', style({opacity: 0, transform: 'translateX(5%)'})), + transition('fromLeft => fromLeftOut', [ + style({opacity: 1, transform: 'translateX(0)'}), + animate('300ms ease-in-out') + ]), + + // Rotate + state('scale', style({opacity: 1, transform: 'scale(1)'})), + transition('* => scale', [ + style({opacity: 0, transform: 'scale(0)'}), + animate('400ms ease-in-out') + ]), + state('scaleOut', style({opacity: 0, transform: 'scale(0)'})), + transition('scale => scaleOut', [ + style({opacity: 1, transform: 'scale(1)'}), + animate('400ms ease-in-out') + ]), + + // Scale + state('rotate', style({opacity: 1, transform: 'rotate(0deg)'})), + transition('* => rotate', [ + style({opacity: 0, transform: 'rotate(5deg)'}), + animate('400ms ease-in-out') + ]), + state('rotateOut', style({opacity: 0, transform: 'rotate(-5deg)'})), + transition('rotate => rotateOut', [ + style({opacity: 1, transform: 'rotate(0deg)'}), + animate('400ms ease-in-out') + ]) + ]) + ], + templateUrl: './notification.component.html', + styleUrls: ['./notification.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class NotificationComponent implements OnInit, OnDestroy { + + @Input() public timeOut: number; + @Input() public showProgressBar: boolean; + @Input() public pauseOnHover: boolean; + @Input() public clickToClose: boolean; + @Input() public clickIconToClose: boolean; + @Input() public maxLength: number; + @Input() public theClass: string; + @Input() public rtl: boolean; + @Input() public animate: string; + @Input() public position: number; + @Input() public item: Notification; + + // Progress bar variables + public title: any; + public content: any; + + public titleIsTemplate = false; + public contentIsTemplate = false; + public htmlIsTemplate = false; + + public progressWidth = 0; + public safeSvg: SafeHtml; + + private stopTime = false; + private timer: any; + private steps: number; + private speed: number; + private count = 0; + private start: any; + + private diff: any; + private icon: string; + + constructor(private notificationService: NotificationsService, + private domSanitizer: DomSanitizer, + private cdr: ChangeDetectorRef, + private zone: NgZone) { + } + + ngOnInit(): void { + if (this.item.override) { + this.attachOverrides(); + } + + if (this.animate) { + this.item.state = this.animate; + } + + if (this.timeOut !== 0) { + this.startTimeOut(); + } + + this.contentType(this.item.title, 'title'); + this.contentType(this.item.content, 'content'); + this.contentType(this.item.html, 'html'); + + this.safeSvg = this.domSanitizer.bypassSecurityTrustHtml(this.icon || this.item.icon); + } + + startTimeOut(): void { + this.steps = this.timeOut / 10; + this.speed = this.timeOut / this.steps; + this.start = new Date().getTime(); + this.zone.runOutsideAngular(() => this.timer = setTimeout(this.instance, this.speed)); + } + + onEnter(): void { + if (this.pauseOnHover) { + this.stopTime = true; + } + } + + onLeave(): void { + if (this.pauseOnHover) { + this.stopTime = false; + this.zone.runOutsideAngular(() => setTimeout(this.instance, (this.speed - this.diff))); + } + } + + onClick($e: MouseEvent): void { + this.item.click!.emit($e); + + if (this.clickToClose) { + this.remove(); + } + } + + onClickIcon($e: MouseEvent): void { + this.item.clickIcon!.emit($e); + + if (this.clickIconToClose) { + this.remove(); + } + } + + // Attach all the overrides + attachOverrides(): void { + Object.keys(this.item.override).forEach((a) => { + if (this.hasOwnProperty(a)) { + (this as any)[a] = this.item.override[a]; + } + }); + } + + ngOnDestroy(): void { + clearTimeout(this.timer); + } + + private instance = () => { + this.diff = (new Date().getTime() - this.start) - (this.count * this.speed); + + if (this.count++ === this.steps) { + this.remove(); + this.item.timeoutEnd!.emit(); + } else if (!this.stopTime) { + if (this.showProgressBar) { + this.progressWidth += 100 / this.steps; + } + + this.timer = setTimeout(this.instance, (this.speed - this.diff)); + } + this.zone.run(() => this.cdr.detectChanges()); + }; + + private remove() { + if (this.animate) { + this.item.state = this.animate + 'Out'; + setTimeout(() => { + this.notificationService.set(this.item, false); + }, 310); + } else { + this.notificationService.set(this.item, false); + } + } + + private contentType(item: any, key: string) { + if (item instanceof TemplateRef) { + this[key] = item; + } else { + this[key] = this.domSanitizer.bypassSecurityTrustHtml(item); + } + + this[key + 'IsTemplate'] = item instanceof TemplateRef; + } +} diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.html b/src/app/shared/notifications/notifications-board/notifications-board.component.html new file mode 100644 index 0000000000..0fa83e3edc --- /dev/null +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.html @@ -0,0 +1,16 @@ +
+ + +
diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.scss b/src/app/shared/notifications/notifications-board/notifications-board.component.scss new file mode 100644 index 0000000000..e95a2a825b --- /dev/null +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.scss @@ -0,0 +1,43 @@ +.simple-notification-wrapper { + position: fixed; + width: 300px; + z-index: 1000; +} + +.simple-notification-wrapper.left { + left: 20px; +} + +.simple-notification-wrapper.top { + top: 20px; +} + +.simple-notification-wrapper.right { + right: 20px; +} + +.simple-notification-wrapper.bottom { + bottom: 20px; +} + +.simple-notification-wrapper.center { + left: 50%; + transform: translateX(-50%); +} + +.simple-notification-wrapper.middle { + top: 50%; + transform: translateY(-50%); +} + +.simple-notification-wrapper.middle.center { + transform: translate(-50%, -50%); +} + +@media (max-width: 340px) { + .simple-notification-wrapper { + width: auto; + left: 20px; + right: 20px; + } +} diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.ts new file mode 100644 index 0000000000..58a68cf980 --- /dev/null +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -0,0 +1,222 @@ +import { + Component, EventEmitter, OnInit, OnDestroy, ViewEncapsulation, Input, Output, + ChangeDetectionStrategy, ChangeDetectorRef +} from '@angular/core'; +import {Subscription} from 'rxjs/Subscription'; +import {Options} from '../interfaces/options.type'; +import {Notification} from '../interfaces/notification.type'; +import {NotificationsService} from '../notifications.service'; + +@Component({ + selector: 'ds-notifications-board', + encapsulation: ViewEncapsulation.None, + templateUrl: './notifications-board.component.html', + styleUrls: ['./notifications-board.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NotificationsBoardComponent implements OnInit, OnDestroy { + + @Input() set options(opt: Options) { + this.attachChanges(opt); + } + + @Output() onCreate = new EventEmitter(); + @Output() onDestroy = new EventEmitter(); + + public notifications: Notification[] = []; + public position: ['top' | 'bottom' | 'middle', 'right' | 'left' | 'center'] = ['bottom', 'right']; + + private lastNotificationCreated: Notification; + private listener: Subscription; + + // Received values + private lastOnBottom = true; + private maxStack = 8; + private preventLastDuplicates: any = false; + private preventDuplicates = false; + + // Sent values + public timeOut = 0; + public maxLength = 0; + public clickToClose = true; + public clickIconToClose = false; + public showProgressBar = true; + public pauseOnHover = true; + public theClass = ''; + public rtl = false; + public animate: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale' = 'fromRight'; + + constructor( + private service: NotificationsService, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit(): void { + // Listen for changes in the service + this.listener = this.service.emitter + .subscribe((item) => { + switch (item.command) { + case 'cleanAll': + this.notifications = []; + break; + + case 'clean': + this.cleanSingle(item.id!); + break; + + case 'set': + if (item.add) { + this.add(item.notification!); + } else { + this.defaultBehavior(item); + } + break; + + default: + this.defaultBehavior(item); + break; + } + + this.cdr.markForCheck(); + }); + } + + // Default behavior on event + defaultBehavior(value: any): void { + this.notifications.splice(this.notifications.indexOf(value.notification), 1); + this.onDestroy.emit(this.buildEmit(value.notification, false)); + } + + // Add the new notification to the notification array + add(item: Notification): void { + item.createdOn = new Date(); + + const toBlock: boolean = this.preventLastDuplicates || this.preventDuplicates ? this.block(item) : false; + + // Save this as the last created notification + this.lastNotificationCreated = item; + // Override icon if set + if (item.override && item.override.icons && item.override.icons[item.type]) { + item.icon = item.override.icons[item.type]; + } + + if (!toBlock) { + // Check if the notification should be added at the start or the end of the array + if (this.lastOnBottom) { + if (this.notifications.length >= this.maxStack) { + this.notifications.splice(0, 1); + } + + this.notifications.push(item); + } else { + if (this.notifications.length >= this.maxStack) { + this.notifications.splice(this.notifications.length - 1, 1); + } + + this.notifications.splice(0, 0, item); + } + + this.onCreate.emit(this.buildEmit(item, true)); + } + } + + // Check if notifications should be prevented + block(item: Notification): boolean { + + const toCheck = item.html ? this.checkHtml : this.checkStandard; + + if (this.preventDuplicates && this.notifications.length > 0) { + for (let i = 0; i < this.notifications.length; i++) { + if (toCheck(this.notifications[i], item)) { + return true; + } + } + } + + if (this.preventLastDuplicates) { + + let comp: Notification; + + if (this.preventLastDuplicates === 'visible' && this.notifications.length > 0) { + if (this.lastOnBottom) { + comp = this.notifications[this.notifications.length - 1]; + } else { + comp = this.notifications[0]; + } + } else if (this.preventLastDuplicates === 'all' && this.lastNotificationCreated) { + comp = this.lastNotificationCreated; + } else { + return false; + } + return toCheck(comp, item); + } + + return false; + } + + checkStandard(checker: Notification, item: Notification): boolean { + return checker.type === item.type && checker.title === item.title && checker.content === item.content; + } + + checkHtml(checker: Notification, item: Notification): boolean { + return checker.html ? checker.type === item.type && checker.title === item.title && checker.content === item.content && checker.html === item.html : false; + } + + // Attach all the changes received in the options object + attachChanges(options: any): void { + Object.keys(options).forEach((a) => { + if (this.hasOwnProperty(a)) { + (this as any)[a] = options[a]; + } else if (a === 'icons') { + this.service.icons = options[a]; + } + }); + } + + buildEmit(notification: Notification, to: boolean) { + const toEmit: Notification = { + createdOn: notification.createdOn, + type: notification.type, + icon: notification.icon, + id: notification.id + }; + + if (notification.html) { + toEmit.html = notification.html; + } else { + toEmit.title = notification.title; + toEmit.content = notification.content; + } + + if (!to) { + toEmit.destroyedOn = new Date(); + } + + return toEmit; + } + + cleanSingle(id: string): void { + let indexOfDelete = 0; + let doDelete = false; + let noti; + + this.notifications.forEach((notification, idx) => { + if (notification.id === id) { + indexOfDelete = idx; + noti = notification; + doDelete = true; + } + }); + + if (doDelete) { + this.notifications.splice(indexOfDelete, 1); + this.onDestroy.emit(this.buildEmit(noti, false)); + } + } + + ngOnDestroy(): void { + if (this.listener) { + this.listener.unsubscribe(); + } + } +} diff --git a/src/app/shared/notifications/notifications.service.spec.ts b/src/app/shared/notifications/notifications.service.spec.ts new file mode 100644 index 0000000000..12bf738b03 --- /dev/null +++ b/src/app/shared/notifications/notifications.service.spec.ts @@ -0,0 +1,263 @@ +import {inject, TestBed} from '@angular/core/testing'; +import {NotificationsService} from './notifications.service'; +import {defaultIcons} from '../interfaces/icons'; +import {NotificationEvent} from '../interfaces/notification-event.type'; +import {Notification} from '../interfaces/notification.type'; + +describe('NotificationsService', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [], + providers: [NotificationsService], + }); + }); + + let defaultNotification = { + id: '0', + title: 'Test title', + type: 'success', + icon: defaultIcons.success, + content: 'Test Content', + timeOut: 0, + maxLength: 0, + clickToClose: true, + clickIconToClose: false, + showProgressBar: true, + pauseOnHover: true, + theClass: 'initial', + rtl: false, + animate: 'fromRight', + createdOn: new Date(), + destroyedOn: new Date() + }; + + it('Service instantiates', + inject([NotificationsService], (service: NotificationsService) => { + expect(service instanceof NotificationsService).toBeTruthy(); + }) + ); + + it('If override is not set, id is randomly generated', + inject([NotificationsService], (service: NotificationsService) => { + + let notificationEvent: NotificationEvent = null; + + service.emitter.subscribe(item => notificationEvent = item); + + service.set(defaultNotification, true); + + expect(notificationEvent.command).toBe('set'); + expect(notificationEvent.notification.id !== '0').toBeTruthy(); + }) + ); + + it('If override id is set its used', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null, + override = {id: '1'}; + + service.emitter.subscribe(item => notificationEvent = item); + + service.set(Object.assign(defaultNotification, {override: override}), true); + + expect(notificationEvent.notification.id).toBe('1'); + }) + ); + + it('Success method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.success('Title', 'Message'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('success'); + expect(notification.icon).toBe(defaultIcons.success); + + expect(notification.title).toBe('Title'); + expect(notification.content).toBe('Message'); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBeUndefined(); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + it('Error method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.error('Title', 'Message'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('error'); + expect(notification.icon).toBe(defaultIcons.error); + + expect(notification.title).toBe('Title'); + expect(notification.content).toBe('Message'); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBeUndefined(); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + + it('Alert method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.alert('Title', 'Message'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('alert'); + expect(notification.icon).toBe(defaultIcons.alert); + + expect(notification.title).toBe('Title'); + expect(notification.content).toBe('Message'); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBeUndefined(); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + + it('Info method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.info('Title', 'Message'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('info'); + expect(notification.icon).toBe(defaultIcons.info); + + expect(notification.title).toBe('Title'); + expect(notification.content).toBe('Message'); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBeUndefined(); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + it('Warn method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.warn('Title', 'Message'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('warn'); + expect(notification.icon).toBe(defaultIcons.warn); + + expect(notification.title).toBe('Title'); + expect(notification.content).toBe('Message'); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBeUndefined(); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + it('Bare method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.bare('Title', 'Message'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('bare'); + expect(notification.icon).toBe('bare'); + + expect(notification.title).toBe('Title'); + expect(notification.content).toBe('Message'); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBeUndefined(); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + + it('Create method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.create('Title', 'Message', 'create'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('create'); + // expect(notification.icon).toBe('bare'); + + expect(notification.title).toBe('Title'); + expect(notification.content).toBe('Message'); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBeUndefined(); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + + it('Html method', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + let notification: Notification = service.html('Title', 'success'); + + expect(notification.id !== undefined).toBeTruthy(); + expect(notification.type).toBe('success'); + expect(notification.icon).toBe('bare'); + + expect(notification.title).toBeUndefined(); + expect(notification.content).toBeUndefined(); + expect(notification.override).toBeUndefined(); + expect(notification.html).toBe('Title'); + expect(notification.state).toBeUndefined(); + expect(notification.createdOn).toBeUndefined(); + expect(notification.destroyedOn).toBeUndefined(); + }) + ); + + it('Empty remove emits cleanAll command', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + service.remove(); + + expect(notificationEvent.command).toBe('cleanAll'); + }) + ); + + it('Remove with id emits clean command', + inject([NotificationsService], (service: NotificationsService) => { + let notificationEvent: NotificationEvent = null; + service.emitter.subscribe(item => notificationEvent = item); + + service.remove('1'); + + expect(notificationEvent.command).toBe('clean'); + expect(notificationEvent.id).toBe('1'); + }) + ); + +}); diff --git a/src/app/shared/notifications/notifications.service.ts b/src/app/shared/notifications/notifications.service.ts new file mode 100644 index 0000000000..6a7974a052 --- /dev/null +++ b/src/app/shared/notifications/notifications.service.ts @@ -0,0 +1,64 @@ +import {Injectable, EventEmitter} from '@angular/core'; +import {Subject} from 'rxjs/Subject'; +import {NotificationEvent} from './interfaces/notification-event.type'; +import {Notification} from './interfaces/notification.type'; +import {Icons, defaultIcons} from './interfaces/icons'; + +@Injectable() +export class NotificationsService { + + public emitter = new Subject(); + public icons: Icons = defaultIcons; + + set(notification: Notification, to: boolean): Notification { + notification.id = notification.override && notification.override.id ? notification.override.id : Math.random().toString(36).substring(3); + notification.click = new EventEmitter<{}>(); + notification.timeoutEnd = new EventEmitter<{}>(); + + this.emitter.next({command: 'set', notification: notification, add: to}); + return notification; + }; + + success(title: any = '', content: any = '', override?: any): Notification { + return this.set({title: title, content: content || '', type: 'success', icon: this.icons.success, override: override}, true); + } + + error(title: any = '', content: any = '', override?: any): Notification { + return this.set({title: title, content: content || '', type: 'error', icon: this.icons.error, override: override}, true); + } + + alert(title: any = '', content: any = '', override?: any): Notification { + return this.set({title: title, content: content || '', type: 'alert', icon: this.icons.alert, override: override}, true); + } + + info(title: any = '', content: any = '', override?: any): Notification { + return this.set({title: title, content: content || '', type: 'info', icon: this.icons.info, override: override}, true); + } + + warn(title: any = '', content: any = '', override?: any): Notification { + return this.set({title: title, content: content || '', type: 'warn', icon: this.icons.warn, override: override}, true); + } + + bare(title: any = '', content: any = '', override?: any): Notification { + return this.set({title: title, content: content || '', type: 'bare', icon: 'bare', override: override}, true); + } + + // With type method + create(title: any = '', content: any = '', type = 'success', override?: any): Notification { + return this.set({title: title, content: content, type: type, icon: (this.icons as any)[type], override: override}, true); + } + + // HTML Notification method + html(html: any, type = 'success', override?: any, icon = 'bare'): Notification { + return this.set({html: html, type: type, icon: (this.icons as any)[icon], override: override}, true); + } + + // Remove all notifications method + remove(id?: string): void { + if (id) { + this.emitter.next({command: 'clean', id: id}); + } else { + this.emitter.next({command: 'cleanAll'}); + } + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index ca13067851..06b871e082 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -21,11 +21,11 @@ import { SearchResultListElementComponent } from './object-list/search-result-li import { WrapperListElementComponent } from './object-list/wrapper-list-element/wrapper-list-element.component'; import { ObjectListComponent } from './object-list/object-list.component'; -import { CollectionGridElementComponent} from './object-grid/collection-grid-element/collection-grid-element.component' -import { CommunityGridElementComponent} from './object-grid/community-grid-element/community-grid-element.component' -import { ItemGridElementComponent} from './object-grid/item-grid-element/item-grid-element.component' -import { AbstractListableElementComponent} from './object-collection/shared/object-collection-element/abstract-listable-element.component' -import { WrapperGridElementComponent} from './object-grid/wrapper-grid-element/wrapper-grid-element.component' +import { CollectionGridElementComponent } from './object-grid/collection-grid-element/collection-grid-element.component'; +import { CommunityGridElementComponent } from './object-grid/community-grid-element/community-grid-element.component'; +import { ItemGridElementComponent } from './object-grid/item-grid-element/item-grid-element.component'; +import { AbstractListableElementComponent } from './object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { WrapperGridElementComponent } from './object-grid/wrapper-grid-element/wrapper-grid-element.component'; import { ObjectGridComponent } from './object-grid/object-grid.component'; import { ObjectCollectionComponent } from './object-collection/object-collection.component'; import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component'; @@ -41,6 +41,8 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component'; import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component'; import { VarDirective } from './utils/var.directive'; +import { NotificationComponent } from './notifications/notification/notification.component'; +import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -79,7 +81,9 @@ const COMPONENTS = [ ThumbnailComponent, GridThumbnailComponent, WrapperListElementComponent, - ViewModeSwitchComponent + ViewModeSwitchComponent, + // NotificationComponent, + // NotificationsBoardComponent ]; const ENTRY_COMPONENTS = [ @@ -91,7 +95,8 @@ const ENTRY_COMPONENTS = [ ItemGridElementComponent, CollectionGridElementComponent, CommunityGridElementComponent, - SearchResultGridElementComponent + SearchResultGridElementComponent, + // NotificationComponent, ]; const DIRECTIVES = [