diff --git a/README.md b/README.md index dd90f08a..9c064cbd 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,8 @@ behaviour: { /** * Defines whether each notification will hide itself automatically after a timeout passes - * @type {number | false} + * support and object with a key per type defaulting to default key. + * @type {number | false | [{key: string]: number | false}} */ autoHide: 5000, @@ -510,7 +511,7 @@ const notifierDefaultOptions: NotifierOptions = { }, theme: 'material', behaviour: { - autoHide: 5000, + autoHide: {default: 5000, info: 3000, error: false}, onClick: false, onMouseover: 'pauseAutoHide', showDismissButton: true, diff --git a/src/demo/app.module.ts b/src/demo/app.module.ts index cfb11d02..7c3e2e3a 100644 --- a/src/demo/app.module.ts +++ b/src/demo/app.module.ts @@ -1,67 +1,67 @@ -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; - -import { NotifierModule, NotifierOptions } from './../lib/index'; - -import { AppComponent } from './app.component'; - -/** - * Custom angular notifier options - */ -const customNotifierOptions: NotifierOptions = { - position: { - horizontal: { - position: 'left', - distance: 12 - }, - vertical: { - position: 'bottom', - distance: 12, - gap: 10 - } - }, - theme: 'material', - behaviour: { - autoHide: false, - onClick: false, - onMouseover: 'pauseAutoHide', - showDismissButton: true, - stacking: 4 - }, - animations: { - enabled: true, - show: { - preset: 'slide', - speed: 300, - easing: 'ease' - }, - hide: { - preset: 'fade', - speed: 300, - easing: 'ease', - offset: 50 - }, - shift: { - speed: 300, - easing: 'ease' - }, - overlap: 150 - } -}; - -/** - * App module - */ -@NgModule( { - bootstrap: [ - AppComponent - ], - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - NotifierModule.withConfig( customNotifierOptions ) - ] -} ) -export class AppModule {} +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { NotifierModule, NotifierOptions } from './../lib/index'; + +import { AppComponent } from './app.component'; + +/** + * Custom angular notifier options + */ +const customNotifierOptions: NotifierOptions = { + position: { + horizontal: { + position: 'left', + distance: 12 + }, + vertical: { + position: 'bottom', + distance: 12, + gap: 10 + } + }, + theme: 'material', + behaviour: { + autoHide: {default: 5000, info: 2000, success: 10000, error: false}, + onClick: false, + onMouseover: 'pauseAutoHide', + showDismissButton: true, + stacking: 4 + }, + animations: { + enabled: true, + show: { + preset: 'slide', + speed: 300, + easing: 'ease' + }, + hide: { + preset: 'fade', + speed: 300, + easing: 'ease', + offset: 50 + }, + shift: { + speed: 300, + easing: 'ease' + }, + overlap: 150 + } +}; + +/** + * App module + */ +@NgModule( { + bootstrap: [ + AppComponent + ], + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + NotifierModule.withConfig( customNotifierOptions ) + ] +} ) +export class AppModule {} diff --git a/src/lib/src/components/notifier-notification.component.spec.ts b/src/lib/src/components/notifier-notification.component.spec.ts index f423c65b..2c1065fa 100644 --- a/src/lib/src/components/notifier-notification.component.spec.ts +++ b/src/lib/src/components/notifier-notification.component.spec.ts @@ -948,6 +948,68 @@ describe( 'Notifier Notification Component', () => { } ) ); + it( 'should extract autoHide values from key', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: {[testNotification.type]: 787, default: 989} + onMouseover: 'pauseAutoHide' + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + jest.spyOn(timerService, 'start'); + componentInstance.show({type: 'info', message: 'test'}); + + expect( timerService.start ).toHaveBeenCalledWith(787); + } ) ); + + it( 'should extract autoHide values from default', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: {default: 989} + onMouseover: 'pauseAutoHide' + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + jest.spyOn(timerService, 'start'); + componentInstance.show({type: 'info', message: 'test'}); + + expect( timerService.start ).toHaveBeenCalledWith(989); + } ) ); + + it( 'should extract autoHide values from fallback', fakeAsync( () => { + + // Setup test module + beforeEachWithConfig( new NotifierConfig( { + animations: { + enabled: false + }, + behaviour: { + autoHide: {} + onMouseover: 'pauseAutoHide' + } + } ) ); + + componentInstance.notification = testNotification; + componentFixture.detectChanges(); + jest.spyOn(timerService, 'start'); + componentInstance.show({type: 'info', message: 'test'}); + + expect( timerService.start ).not.toHaveBeenCalled(); + } ) ); } ); /** diff --git a/src/lib/src/components/notifier-notification.component.ts b/src/lib/src/components/notifier-notification.component.ts index 65af2ddb..8963ca80 100644 --- a/src/lib/src/components/notifier-notification.component.ts +++ b/src/lib/src/components/notifier-notification.component.ts @@ -1,383 +1,407 @@ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core'; - -import { NotifierAnimationData } from './../models/notifier-animation.model'; -import { NotifierAnimationService } from './../services/notifier-animation.service'; -import { NotifierConfig } from './../models/notifier-config.model'; -import { NotifierNotification } from './../models/notifier-notification.model'; -import { NotifierService } from './../services/notifier.service'; -import { NotifierTimerService } from './../services/notifier-timer.service'; - -/** - * Notifier notification component - * ------------------------------- - * This component is responsible for actually displaying the notification on screen. In addition, it's able to show and hide this - * notification, in particular to animate this notification in and out, as well as shift (move) this notification vertically around. - * Furthermore, the notification component handles all interactions the user has with this notification / component, such as clicks and - * mouse movements. - */ -@Component( { - changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters) - host: { - '(click)': 'onNotificationClick()', - '(mouseout)': 'onNotificationMouseout()', - '(mouseover)': 'onNotificationMouseover()', - class: 'notifier__notification' - }, - providers: [ - // We provide the timer to the component's local injector, so that every notification components gets its own - // instance of the timer service, thus running their timers independently from each other - NotifierTimerService - ], - selector: 'notifier-notification', - templateUrl: './notifier-notification.component.html' -} ) -export class NotifierNotificationComponent implements AfterViewInit { - - /** - * Input: Notification object, contains all details necessary to construct the notification - */ - @Input() - public notification: NotifierNotification; - - /** - * Output: Ready event, handles the initialization success by emitting a reference to this notification component - */ - @Output() - public ready: EventEmitter; - - /** - * Output: Dismiss event, handles the click on the dismiss button by emitting the notification ID of this notification component - */ - @Output() - public dismiss: EventEmitter; - - /** - * Notifier configuration - */ - public readonly config: NotifierConfig; - - /** - * Notifier timer service - */ - private readonly timerService: NotifierTimerService; - - /** - * Notifier animation service - */ - private readonly animationService: NotifierAnimationService; - - /** - * Angular renderer, used to preserve the overall DOM abstraction & independence - */ - private readonly renderer: Renderer2; - - /** - * Native element reference, used for manipulating DOM properties - */ - private readonly element: HTMLElement; - - /** - * Current notification height, calculated and cached here (#perfmatters) - */ - private elementHeight: number; - - /** - * Current notification width, calculated and cached here (#perfmatters) - */ - private elementWidth: number; - - /** - * Current notification shift, calculated and cached here (#perfmatters) - */ - private elementShift: number; - - /** - * Constructor - * - * @param elementRef Reference to the component's element - * @param renderer Angular renderer - * @param notifierService Notifier service - * @param notifierTimerService Notifier timer service - * @param notifierAnimationService Notifier animation service - */ - public constructor( elementRef: ElementRef, renderer: Renderer2, notifierService: NotifierService, - notifierTimerService: NotifierTimerService, notifierAnimationService: NotifierAnimationService ) { - this.config = notifierService.getConfig(); - this.ready = new EventEmitter(); - this.dismiss = new EventEmitter(); - this.timerService = notifierTimerService; - this.animationService = notifierAnimationService; - this.renderer = renderer; - this.element = elementRef.nativeElement; - this.elementShift = 0; - } - - /** - * Component after view init lifecycle hook, setts up the component and then emits the ready event - */ - public ngAfterViewInit(): void { - this.setup(); - this.elementHeight = this.element.offsetHeight; - this.elementWidth = this.element.offsetWidth; - this.ready.emit( this ); - } - - /** - * Get the notifier config - * - * @returns Notifier configuration - */ - public getConfig(): NotifierConfig { - return this.config; - } - - /** - * Get notification element height (in px) - * - * @returns Notification element height (in px) - */ - public getHeight(): number { - return this.elementHeight; - } - - /** - * Get notification element width (in px) - * - * @returns Notification element height (in px) - */ - public getWidth(): number { - return this.elementWidth; - } - - /** - * Get notification shift offset (in px) - * - * @returns Notification element shift offset (in px) - */ - public getShift(): number { - return this.elementShift; - } - - /** - * Show (animate in) this notification - * - * @returns Promise, resolved when done - */ - public show(): Promise { - return new Promise( ( resolve: () => void, reject: () => void ) => { - - // Are animations enabled? - if ( this.config.animations.enabled && this.config.animations.show.speed > 0 ) { - - // Get animation data - const animationData: NotifierAnimationData = this.animationService.getAnimationData( 'show', this.notification ); - - // Set initial styles (styles before animation), prevents quick flicker when animation starts - const animatedProperties: Array = Object.keys( animationData.keyframes[ 0 ] ); - for ( let i: number = animatedProperties.length - 1; i >= 0; i-- ) { - this.renderer.setStyle( this.element, animatedProperties[ i ], - animationData.keyframes[ 0 ][ animatedProperties[ i ] ] ); - } - - // Animate notification in - this.renderer.setStyle( this.element, 'visibility', 'visible' ); - const animation: Animation = this.element.animate( animationData.keyframes, animationData.options ); - animation.onfinish = () => { - this.startAutoHideTimer(); - resolve(); // Done - }; - - } else { - - // Show notification - this.renderer.setStyle( this.element, 'visibility', 'visible' ); - this.startAutoHideTimer(); - resolve(); // Done - - } - - } ); - - } - - /** - * Hide (animate out) this notification - * - * @returns Promise, resolved when done - */ - public hide(): Promise { - return new Promise( ( resolve: () => void, reject: () => void ) => { - - this.stopAutoHideTimer(); - - // Are animations enabled? - if ( this.config.animations.enabled && this.config.animations.hide.speed > 0 ) { - const animationData: NotifierAnimationData = this.animationService.getAnimationData( 'hide', this.notification ); - const animation: Animation = this.element.animate( animationData.keyframes, animationData.options ); - animation.onfinish = () => { - resolve(); // Done - }; - } else { - resolve(); // Done - } - - } ); - } - - /** - * Shift (move) this notification - * - * @param distance Distance to shift (in px) - * @param shiftToMakePlace Flag, defining in which direction to shift - * @returns Promise, resolved when done - */ - public shift( distance: number, shiftToMakePlace: boolean ): Promise { - return new Promise( ( resolve: () => void, reject: () => void ) => { - - // Calculate new position (position after the shift) - let newElementShift: number; - if ( ( this.config.position.vertical.position === 'top' && shiftToMakePlace ) - || ( this.config.position.vertical.position === 'bottom' && !shiftToMakePlace ) ) { - newElementShift = this.elementShift + distance + this.config.position.vertical.gap; - } else { - newElementShift = this.elementShift - distance - this.config.position.vertical.gap; - } - const horizontalPosition: string = this.config.position.horizontal.position === 'middle' ? '-50%' : '0'; - - // Are animations enabled? - if ( this.config.animations.enabled && this.config.animations.shift.speed > 0 ) { - const animationData: NotifierAnimationData = { // TODO: Extract into animation service - keyframes: [ - { - transform: `translate3d( ${ horizontalPosition }, ${ this.elementShift }px, 0 )` - }, - { - transform: `translate3d( ${ horizontalPosition }, ${ newElementShift }px, 0 )` - } - ], - options: { - duration: this.config.animations.shift.speed, - easing: this.config.animations.shift.easing, - fill: 'forwards' - } - }; - this.elementShift = newElementShift; - const animation: Animation = this.element.animate( animationData.keyframes, animationData.options ); - animation.onfinish = () => { - resolve(); // Done - }; - - } else { - this.renderer.setStyle( this.element, 'transform', `translate3d( ${ horizontalPosition }, ${ newElementShift }px, 0 )` ); - this.elementShift = newElementShift; - resolve(); // Done - } - - } ); - - } - - /** - * Handle click on dismiss button - */ - public onClickDismiss(): void { - this.dismiss.emit( this.notification.id ); - } - - /** - * Handle mouseover over notification area - */ - public onNotificationMouseover(): void { - if ( this.config.behaviour.onMouseover === 'pauseAutoHide' ) { - this.pauseAutoHideTimer(); - } else if ( this.config.behaviour.onMouseover === 'resetAutoHide' ) { - this.stopAutoHideTimer(); - } - } - - /** - * Handle mouseout from notification area - */ - public onNotificationMouseout(): void { - if ( this.config.behaviour.onMouseover === 'pauseAutoHide' ) { - this.continueAutoHideTimer(); - } else if ( this.config.behaviour.onMouseover === 'resetAutoHide' ) { - this.startAutoHideTimer(); - } - } - - /** - * Handle click on notification area - */ - public onNotificationClick(): void { - if ( this.config.behaviour.onClick === 'hide' ) { - this.onClickDismiss(); - } - } - - /** - * Start the auto hide timer (if enabled) - */ - private startAutoHideTimer(): void { - if ( this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0 ) { - this.timerService.start( this.config.behaviour.autoHide ).then( () => { - this.onClickDismiss(); - } ); - } - } - - /** - * Pause the auto hide timer (if enabled) - */ - private pauseAutoHideTimer(): void { - if ( this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0 ) { - this.timerService.pause(); - } - } - - /** - * Continue the auto hide timer (if enabled) - */ - private continueAutoHideTimer(): void { - if ( this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0 ) { - this.timerService.continue(); - } - } - - /** - * Stop the auto hide timer (if enabled) - */ - private stopAutoHideTimer(): void { - if ( this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0 ) { - this.timerService.stop(); - } - } - - /** - * Initial notification setup - */ - private setup(): void { - - // Set start position (initially the exact same for every new notification) - if ( this.config.position.horizontal.position === 'left' ) { - this.renderer.setStyle( this.element, 'left', `${ this.config.position.horizontal.distance }px` ); - } else if ( this.config.position.horizontal.position === 'right' ) { - this.renderer.setStyle( this.element, 'right', `${ this.config.position.horizontal.distance }px` ); - } else { - this.renderer.setStyle( this.element, 'left', '50%' ); - // Let's get the GPU handle some work as well (#perfmatters) - this.renderer.setStyle( this.element, 'transform', 'translate3d( -50%, 0, 0 )' ); - } - if ( this.config.position.vertical.position === 'top' ) { - this.renderer.setStyle( this.element, 'top', `${ this.config.position.vertical.distance }px` ); - } else { - this.renderer.setStyle( this.element, 'bottom', `${ this.config.position.vertical.distance }px` ); - } - - // Add classes (responsible for visual design) - this.renderer.addClass( this.element, `notifier__notification--${ this.notification.type }` ); - this.renderer.addClass( this.element, `notifier__notification--${ this.config.theme }` ); - - } - -} +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core'; + +import { NotifierAnimationData } from './../models/notifier-animation.model'; +import { NotifierAnimationService } from './../services/notifier-animation.service'; +import { NotifierConfig } from './../models/notifier-config.model'; +import { NotifierNotification } from './../models/notifier-notification.model'; +import { NotifierService } from './../services/notifier.service'; +import { NotifierTimerService } from './../services/notifier-timer.service'; + +/** + * Notifier notification component + * ------------------------------- + * This component is responsible for actually displaying the notification on screen. In addition, it's able to show and hide this + * notification, in particular to animate this notification in and out, as well as shift (move) this notification vertically around. + * Furthermore, the notification component handles all interactions the user has with this notification / component, such as clicks and + * mouse movements. + */ +@Component( { + changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters) + host: { + '(click)': 'onNotificationClick()', + '(mouseout)': 'onNotificationMouseout()', + '(mouseover)': 'onNotificationMouseover()', + class: 'notifier__notification' + }, + providers: [ + // We provide the timer to the component's local injector, so that every notification components gets its own + // instance of the timer service, thus running their timers independently from each other + NotifierTimerService + ], + selector: 'notifier-notification', + templateUrl: './notifier-notification.component.html' +} ) +export class NotifierNotificationComponent implements AfterViewInit { + + /** + * Input: Notification object, contains all details necessary to construct the notification + */ + @Input() + public notification: NotifierNotification; + + /** + * Output: Ready event, handles the initialization success by emitting a reference to this notification component + */ + @Output() + public ready: EventEmitter; + + /** + * Output: Dismiss event, handles the click on the dismiss button by emitting the notification ID of this notification component + */ + @Output() + public dismiss: EventEmitter; + + /** + * Notifier configuration + */ + public readonly config: NotifierConfig; + + /** + * Notifier timer service + */ + private readonly timerService: NotifierTimerService; + + /** + * Notifier animation service + */ + private readonly animationService: NotifierAnimationService; + + /** + * Angular renderer, used to preserve the overall DOM abstraction & independence + */ + private readonly renderer: Renderer2; + + /** + * Native element reference, used for manipulating DOM properties + */ + private readonly element: HTMLElement; + + /** + * Current notification height, calculated and cached here (#perfmatters) + */ + private elementHeight: number; + + /** + * Current notification width, calculated and cached here (#perfmatters) + */ + private elementWidth: number; + + /** + * Current notification shift, calculated and cached here (#perfmatters) + */ + private elementShift: number; + + /** + * Constructor + * + * @param elementRef Reference to the component's element + * @param renderer Angular renderer + * @param notifierService Notifier service + * @param notifierTimerService Notifier timer service + * @param notifierAnimationService Notifier animation service + */ + public constructor( elementRef: ElementRef, renderer: Renderer2, notifierService: NotifierService, + notifierTimerService: NotifierTimerService, notifierAnimationService: NotifierAnimationService ) { + this.config = notifierService.getConfig(); + this.ready = new EventEmitter(); + this.dismiss = new EventEmitter(); + this.timerService = notifierTimerService; + this.animationService = notifierAnimationService; + this.renderer = renderer; + this.element = elementRef.nativeElement; + this.elementShift = 0; + } + + /** + * Component after view init lifecycle hook, setts up the component and then emits the ready event + */ + public ngAfterViewInit(): void { + this.setup(); + this.elementHeight = this.element.offsetHeight; + this.elementWidth = this.element.offsetWidth; + this.ready.emit( this ); + } + + /** + * Get the notifier config + * + * @returns Notifier configuration + */ + public getConfig(): NotifierConfig { + return this.config; + } + + /** + * Get notification element height (in px) + * + * @returns Notification element height (in px) + */ + public getHeight(): number { + return this.elementHeight; + } + + /** + * Get notification element width (in px) + * + * @returns Notification element height (in px) + */ + public getWidth(): number { + return this.elementWidth; + } + + /** + * Get notification shift offset (in px) + * + * @returns Notification element shift offset (in px) + */ + public getShift(): number { + return this.elementShift; + } + + /** + * Show (animate in) this notification + * + * @returns Promise, resolved when done + */ + public show(): Promise { + return new Promise( ( resolve: () => void, reject: () => void ) => { + + // Are animations enabled? + if ( this.config.animations.enabled && this.config.animations.show.speed > 0 ) { + + // Get animation data + const animationData: NotifierAnimationData = this.animationService.getAnimationData( 'show', this.notification ); + + // Set initial styles (styles before animation), prevents quick flicker when animation starts + const animatedProperties: Array = Object.keys( animationData.keyframes[ 0 ] ); + for ( let i: number = animatedProperties.length - 1; i >= 0; i-- ) { + this.renderer.setStyle( this.element, animatedProperties[ i ], + animationData.keyframes[ 0 ][ animatedProperties[ i ] ] ); + } + + // Animate notification in + this.renderer.setStyle( this.element, 'visibility', 'visible' ); + const animation: Animation = this.element.animate( animationData.keyframes, animationData.options ); + animation.onfinish = () => { + this.startAutoHideTimer(); + resolve(); // Done + }; + + } else { + + // Show notification + this.renderer.setStyle( this.element, 'visibility', 'visible' ); + this.startAutoHideTimer(); + resolve(); // Done + + } + + } ); + + } + + /** + * Hide (animate out) this notification + * + * @returns Promise, resolved when done + */ + public hide(): Promise { + return new Promise( ( resolve: () => void, reject: () => void ) => { + + this.stopAutoHideTimer(); + + // Are animations enabled? + if ( this.config.animations.enabled && this.config.animations.hide.speed > 0 ) { + const animationData: NotifierAnimationData = this.animationService.getAnimationData( 'hide', this.notification ); + const animation: Animation = this.element.animate( animationData.keyframes, animationData.options ); + animation.onfinish = () => { + resolve(); // Done + }; + } else { + resolve(); // Done + } + + } ); + } + + /** + * Shift (move) this notification + * + * @param distance Distance to shift (in px) + * @param shiftToMakePlace Flag, defining in which direction to shift + * @returns Promise, resolved when done + */ + public shift( distance: number, shiftToMakePlace: boolean ): Promise { + return new Promise( ( resolve: () => void, reject: () => void ) => { + + // Calculate new position (position after the shift) + let newElementShift: number; + if ( ( this.config.position.vertical.position === 'top' && shiftToMakePlace ) + || ( this.config.position.vertical.position === 'bottom' && !shiftToMakePlace ) ) { + newElementShift = this.elementShift + distance + this.config.position.vertical.gap; + } else { + newElementShift = this.elementShift - distance - this.config.position.vertical.gap; + } + const horizontalPosition: string = this.config.position.horizontal.position === 'middle' ? '-50%' : '0'; + + // Are animations enabled? + if ( this.config.animations.enabled && this.config.animations.shift.speed > 0 ) { + const animationData: NotifierAnimationData = { // TODO: Extract into animation service + keyframes: [ + { + transform: `translate3d( ${ horizontalPosition }, ${ this.elementShift }px, 0 )` + }, + { + transform: `translate3d( ${ horizontalPosition }, ${ newElementShift }px, 0 )` + } + ], + options: { + duration: this.config.animations.shift.speed, + easing: this.config.animations.shift.easing, + fill: 'forwards' + } + }; + this.elementShift = newElementShift; + const animation: Animation = this.element.animate( animationData.keyframes, animationData.options ); + animation.onfinish = () => { + resolve(); // Done + }; + + } else { + this.renderer.setStyle( this.element, 'transform', `translate3d( ${ horizontalPosition }, ${ newElementShift }px, 0 )` ); + this.elementShift = newElementShift; + resolve(); // Done + } + + } ); + + } + + /** + * Handle click on dismiss button + */ + public onClickDismiss(): void { + this.dismiss.emit( this.notification.id ); + } + + /** + * Handle mouseover over notification area + */ + public onNotificationMouseover(): void { + if ( this.config.behaviour.onMouseover === 'pauseAutoHide' ) { + this.pauseAutoHideTimer(); + } else if ( this.config.behaviour.onMouseover === 'resetAutoHide' ) { + this.stopAutoHideTimer(); + } + } + + /** + * Handle mouseout from notification area + */ + public onNotificationMouseout(): void { + if ( this.config.behaviour.onMouseover === 'pauseAutoHide' ) { + this.continueAutoHideTimer(); + } else if ( this.config.behaviour.onMouseover === 'resetAutoHide' ) { + this.startAutoHideTimer(); + } + } + + /** + * Handle click on notification area + */ + public onNotificationClick(): void { + if ( this.config.behaviour.onClick === 'hide' ) { + this.onClickDismiss(); + } + } + + /** + * Get auto hide value, return simple values (e.g. boolean, number), + * or extract the value if autoHide is an object, defaulting to the default + * key or false. + */ + private getAutoHide(behaviour, type: string) { + if(typeof(behaviour.autoHide) === 'object') { + return behaviour.autoHide[type] !== undefined ? + behaviour.autoHide[type] : + behaviour.autoHide.default ? + behaviour.autoHide.default : + false; + } + return behaviour.autoHide; + } + + private shouldAutoHide(autoHide): boolean { + return autoHide !== false && autoHide > 0; + } + + /** + * Start the auto hide timer (if enabled) + */ + private startAutoHideTimer(): void { + const autoHide = this.getAutoHide(this.config.behaviour, this.notification.type); + if (this.shouldAutoHide(autoHide)) { + this.timerService.start(autoHide).then( () => { + this.onClickDismiss(); + } ); + } + } + + /** + * Pause the auto hide timer (if enabled) + */ + private pauseAutoHideTimer(): void { + const autoHide = this.getAutoHide(this.config.behaviour, this.notification.type); + if (this.shouldAutoHide(autoHide)) { + this.timerService.pause(); + } + } + + /** + * Continue the auto hide timer (if enabled) + */ + private continueAutoHideTimer(): void { + const autoHide = this.getAutoHide(this.config.behaviour, this.notification.type); + if (this.shouldAutoHide(autoHide)) { + this.timerService.continue(); + } + } + + /** + * Stop the auto hide timer (if enabled) + */ + private stopAutoHideTimer(): void { + const autoHide = this.getAutoHide(this.config.behaviour, this.notification.type); + if (this.shouldAutoHide(autoHide)) { + this.timerService.stop(); + } + } + + /** + * Initial notification setup + */ + private setup(): void { + + // Set start position (initially the exact same for every new notification) + if ( this.config.position.horizontal.position === 'left' ) { + this.renderer.setStyle( this.element, 'left', `${ this.config.position.horizontal.distance }px` ); + } else if ( this.config.position.horizontal.position === 'right' ) { + this.renderer.setStyle( this.element, 'right', `${ this.config.position.horizontal.distance }px` ); + } else { + this.renderer.setStyle( this.element, 'left', '50%' ); + // Let's get the GPU handle some work as well (#perfmatters) + this.renderer.setStyle( this.element, 'transform', 'translate3d( -50%, 0, 0 )' ); + } + if ( this.config.position.vertical.position === 'top' ) { + this.renderer.setStyle( this.element, 'top', `${ this.config.position.vertical.distance }px` ); + } else { + this.renderer.setStyle( this.element, 'bottom', `${ this.config.position.vertical.distance }px` ); + } + + // Add classes (responsible for visual design) + this.renderer.addClass( this.element, `notifier__notification--${ this.notification.type }` ); + this.renderer.addClass( this.element, `notifier__notification--${ this.config.theme }` ); + + } + +} diff --git a/src/lib/src/models/notifier-config.model.ts b/src/lib/src/models/notifier-config.model.ts index c15241bc..47683d87 100644 --- a/src/lib/src/models/notifier-config.model.ts +++ b/src/lib/src/models/notifier-config.model.ts @@ -1,192 +1,192 @@ -/** - * Notifier options - */ -export interface NotifierOptions { - animations?: { - enabled?: boolean; - hide?: { - easing?: string; - offset?: number | false; - preset?: string; - speed?: number; - }; - overlap?: number | false; - shift?: { - easing?: string; - speed?: number; - }; - show?: { - easing?: string; - preset?: string; - speed?: number; - }; - }; - behaviour?: { - autoHide?: number | false; - onClick?: 'hide' | false; - onMouseover?: 'pauseAutoHide' | 'resetAutoHide' | false; - showDismissButton?: boolean; - stacking?: number | false; - }; - position?: { - horizontal?: { - distance?: number; - position?: 'left' | 'middle' | 'right'; - }; - vertical?: { - distance?: number; - gap?: number; - position?: 'top' | 'bottom'; - }; - }; - theme?: string; -} - -/** - * Notifier configuration - * - * The notifier configuration defines what notifications look like, how they behave, and how they get animated. It is a global - * configuration, which means that it only can be set once (at the beginning), and cannot be changed afterwards. Aligning to the world of - * Angular, this configuration can be provided in the root app module - alternatively, a meaningful default configuration will be used. - */ -export class NotifierConfig implements NotifierOptions { - - /** - * Customize animations - */ - public animations: { - enabled: boolean; - hide: { - easing: string; - offset: number | false; - preset: string; - speed: number; - }; - overlap: number | false; - shift: { - easing: string; - speed: number; - }; - show: { - easing: string; - preset: string; - speed: number; - }; - }; - - /** - * Customize behaviour - */ - public behaviour: { - autoHide: number | false; - onClick: 'hide' | false; - onMouseover: 'pauseAutoHide' | 'resetAutoHide' | false; - showDismissButton: boolean; - stacking: number | false; - }; - - /** - * Customize positioning - */ - public position: { - horizontal: { - distance: number; - position: 'left' | 'middle' | 'right'; - }; - vertical: { - distance: number; - gap: number; - position: 'top' | 'bottom'; - }; - }; - - /** - * Customize theming - */ - public theme: string; - - /** - * Constructor - * - * @param [customOptions={}] Custom notifier options, optional - */ - public constructor( customOptions: NotifierOptions = {} ) { - - // Set default values - this.animations = { - enabled: true, - hide: { - easing: 'ease', - offset: 50, - preset: 'fade', - speed: 300 - }, - overlap: 150, - shift: { - easing: 'ease', - speed: 300 - }, - show: { - easing: 'ease', - preset: 'slide', - speed: 300 - } - }; - this.behaviour = { - autoHide: 7000, - onClick: false, - onMouseover: 'pauseAutoHide', - showDismissButton: true, - stacking: 4 - }; - this.position = { - horizontal: { - distance: 12, - position: 'left' - }, - vertical: { - distance: 12, - gap: 10, - position: 'bottom' - } - }; - this.theme = 'material'; - - // The following merges the custom options into the notifier config, respecting the already set default values - // This linear, more explicit and code-sizy workflow is preferred here over a recursive one (because we know the object structure) - // Technical sidenote: Objects are merged, other types of values simply overwritten / copied - if ( customOptions.theme !== undefined ) { - this.theme = customOptions.theme; - } - if ( customOptions.animations !== undefined ) { - if ( customOptions.animations.enabled !== undefined ) { - this.animations.enabled = customOptions.animations.enabled; - } - if ( customOptions.animations.overlap !== undefined ) { - this.animations.overlap = customOptions.animations.overlap; - } - if ( customOptions.animations.hide !== undefined ) { - Object.assign( this.animations.hide, customOptions.animations.hide ); - } - if ( customOptions.animations.shift !== undefined ) { - Object.assign( this.animations.shift, customOptions.animations.shift ); - } - if ( customOptions.animations.show !== undefined ) { - Object.assign( this.animations.show, customOptions.animations.show ); - } - } - if ( customOptions.behaviour !== undefined ) { - Object.assign( this.behaviour, customOptions.behaviour ); - } - if ( customOptions.position !== undefined ) { - if ( customOptions.position.horizontal !== undefined ) { - Object.assign( this.position.horizontal, customOptions.position.horizontal ); - } - if ( customOptions.position.vertical !== undefined ) { - Object.assign( this.position.vertical, customOptions.position.vertical ); - } - } - - } - -} +/** + * Notifier options + */ +export interface NotifierOptions { + animations?: { + enabled?: boolean; + hide?: { + easing?: string; + offset?: number | false; + preset?: string; + speed?: number; + }; + overlap?: number | false; + shift?: { + easing?: string; + speed?: number; + }; + show?: { + easing?: string; + preset?: string; + speed?: number; + }; + }; + behaviour?: { + autoHide?: number | false | {[key: string]: number | false}; + onClick?: 'hide' | false; + onMouseover?: 'pauseAutoHide' | 'resetAutoHide' | false; + showDismissButton?: boolean; + stacking?: number | false; + }; + position?: { + horizontal?: { + distance?: number; + position?: 'left' | 'middle' | 'right'; + }; + vertical?: { + distance?: number; + gap?: number; + position?: 'top' | 'bottom'; + }; + }; + theme?: string; +} + +/** + * Notifier configuration + * + * The notifier configuration defines what notifications look like, how they behave, and how they get animated. It is a global + * configuration, which means that it only can be set once (at the beginning), and cannot be changed afterwards. Aligning to the world of + * Angular, this configuration can be provided in the root app module - alternatively, a meaningful default configuration will be used. + */ +export class NotifierConfig implements NotifierOptions { + + /** + * Customize animations + */ + public animations: { + enabled: boolean; + hide: { + easing: string; + offset: number | false; + preset: string; + speed: number; + }; + overlap: number | false; + shift: { + easing: string; + speed: number; + }; + show: { + easing: string; + preset: string; + speed: number; + }; + }; + + /** + * Customize behaviour + */ + public behaviour: { + autoHide: number | false | {[key: string]: number | false}; + onClick: 'hide' | false; + onMouseover: 'pauseAutoHide' | 'resetAutoHide' | false; + showDismissButton: boolean; + stacking: number | false; + }; + + /** + * Customize positioning + */ + public position: { + horizontal: { + distance: number; + position: 'left' | 'middle' | 'right'; + }; + vertical: { + distance: number; + gap: number; + position: 'top' | 'bottom'; + }; + }; + + /** + * Customize theming + */ + public theme: string; + + /** + * Constructor + * + * @param [customOptions={}] Custom notifier options, optional + */ + public constructor( customOptions: NotifierOptions = {} ) { + + // Set default values + this.animations = { + enabled: true, + hide: { + easing: 'ease', + offset: 50, + preset: 'fade', + speed: 300 + }, + overlap: 150, + shift: { + easing: 'ease', + speed: 300 + }, + show: { + easing: 'ease', + preset: 'slide', + speed: 300 + } + }; + this.behaviour = { + autoHide: 7000, + onClick: false, + onMouseover: 'pauseAutoHide', + showDismissButton: true, + stacking: 4 + }; + this.position = { + horizontal: { + distance: 12, + position: 'left' + }, + vertical: { + distance: 12, + gap: 10, + position: 'bottom' + } + }; + this.theme = 'material'; + + // The following merges the custom options into the notifier config, respecting the already set default values + // This linear, more explicit and code-sizy workflow is preferred here over a recursive one (because we know the object structure) + // Technical sidenote: Objects are merged, other types of values simply overwritten / copied + if ( customOptions.theme !== undefined ) { + this.theme = customOptions.theme; + } + if ( customOptions.animations !== undefined ) { + if ( customOptions.animations.enabled !== undefined ) { + this.animations.enabled = customOptions.animations.enabled; + } + if ( customOptions.animations.overlap !== undefined ) { + this.animations.overlap = customOptions.animations.overlap; + } + if ( customOptions.animations.hide !== undefined ) { + Object.assign( this.animations.hide, customOptions.animations.hide ); + } + if ( customOptions.animations.shift !== undefined ) { + Object.assign( this.animations.shift, customOptions.animations.shift ); + } + if ( customOptions.animations.show !== undefined ) { + Object.assign( this.animations.show, customOptions.animations.show ); + } + } + if ( customOptions.behaviour !== undefined ) { + Object.assign( this.behaviour, customOptions.behaviour ); + } + if ( customOptions.position !== undefined ) { + if ( customOptions.position.horizontal !== undefined ) { + Object.assign( this.position.horizontal, customOptions.position.horizontal ); + } + if ( customOptions.position.vertical !== undefined ) { + Object.assign( this.position.vertical, customOptions.position.vertical ); + } + } + + } + +}