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 = [