UNPKG

gramli-angular-notifier

Version:

A well designed, fully animated, highly customizable, and easy-to-use notification library for your Angular application.

1 lines 93 kB
{"version":3,"file":"gramli-angular-notifier.mjs","sources":["../../../projects/angular-notifier/src/lib/models/notifier-notification.model.ts","../../../projects/angular-notifier/src/lib/notifier.tokens.ts","../../../projects/angular-notifier/src/lib/services/notifier-queue.service.ts","../../../projects/angular-notifier/src/lib/services/notifier.service.ts","../../../projects/angular-notifier/src/lib/animation-presets/fade.animation-preset.ts","../../../projects/angular-notifier/src/lib/animation-presets/slide.animation-preset.ts","../../../projects/angular-notifier/src/lib/services/notifier-animation.service.ts","../../../projects/angular-notifier/src/lib/services/notifier-timer.service.ts","../../../projects/angular-notifier/src/lib/components/notifier-notification.component.ts","../../../projects/angular-notifier/src/lib/components/notifier-notification.component.html","../../../projects/angular-notifier/src/lib/components/notifier-container.component.ts","../../../projects/angular-notifier/src/lib/components/notifier-container.component.html","../../../projects/angular-notifier/src/lib/models/notifier-config.model.ts","../../../projects/angular-notifier/src/lib/notifier.module.ts","../../../projects/angular-notifier/src/gramli-angular-notifier.ts"],"sourcesContent":["import { TemplateRef } from '@angular/core';\n\nimport { NotifierNotificationComponent } from '../components/notifier-notification.component';\n\n/**\n * Notification\n *\n * This class describes the structure of a notifiction, including all information it needs to live, and everyone else needs to work with it.\n */\nexport class NotifierNotification {\n /**\n * Unique notification ID, can be set manually to control the notification from outside later on\n */\n public id: string;\n\n /**\n * Notification type, will be used for constructing an appropriate class name\n */\n public type: string;\n\n /**\n * Notification message\n */\n public message: string;\n\n /**\n * The template to customize\n * the appearance of the notification\n */\n public template?: TemplateRef<any> = null;\n\n /**\n * Component reference of this notification, created and set during creation time\n */\n public component: NotifierNotificationComponent;\n\n /**\n * Constructor\n *\n * @param options Notifier options\n */\n public constructor(options: NotifierNotificationOptions) {\n Object.assign(this, options);\n\n // If not set manually, we have to create a unique notification ID by ourselves. The ID generation relies on the current browser\n // datetime in ms, in praticular the moment this notification gets constructed. Concurrency, and thus two IDs being the exact same,\n // is not possible due to the action queue concept.\n if (options.id === undefined) {\n this.id = `ID_${new Date().getTime()}`;\n }\n }\n}\n\n/**\n * Notifiction options\n *\n * This interface describes which information are needed to create a new notification, or in other words, which information the external API\n * call must provide.\n */\nexport interface NotifierNotificationOptions {\n /**\n * Notification ID, optional\n */\n id?: string;\n\n /**\n * Notification type\n */\n type: string;\n\n /**\n * Notificatin message\n */\n message: string;\n\n /**\n * The template to customize\n * the appearance of the notification\n */\n template?: TemplateRef<any>;\n}\n","import { InjectionToken } from '@angular/core';\n\nimport { NotifierConfig, NotifierOptions } from './models/notifier-config.model';\n\n/**\n * Injection Token for notifier options\n */\nexport const NotifierOptionsToken: InjectionToken<NotifierOptions> = new InjectionToken<NotifierOptions>(\n '[angular-notifier] Notifier Options',\n);\n\n/**\n * Injection Token for notifier configuration\n */\nexport const NotifierConfigToken: InjectionToken<NotifierConfig> = new InjectionToken<NotifierConfig>('[anuglar-notifier] Notifier Config');\n","import { Injectable } from '@angular/core';\nimport { Subject } from 'rxjs';\n\nimport { NotifierAction } from '../models/notifier-action.model';\n\n/**\n * Notifier queue service\n *\n * In general, API calls don't get processed right away. Instead, we have to queue them up in order to prevent simultanious API calls\n * interfering with each other. This, at least in theory, is possible at any time. In particular, animations - which potentially overlap -\n * can cause changes in JS classes as well as affect the DOM. Therefore, the queue service takes all actions, puts them in a queue, and\n * processes them at the right time (which is when the previous action has been processed successfully).\n *\n * Technical sidenote:\n * An action looks pretty similar to the ones within the Flux / Redux pattern.\n */\n@Injectable()\nexport class NotifierQueueService {\n /**\n * Stream of actions, subscribable from outside\n */\n public readonly actionStream: Subject<NotifierAction>;\n\n /**\n * Queue of actions\n */\n private actionQueue: Array<NotifierAction>;\n\n /**\n * Flag, true if some action is currently in progress\n */\n private isActionInProgress: boolean;\n\n /**\n * Constructor\n */\n public constructor() {\n this.actionStream = new Subject<NotifierAction>();\n this.actionQueue = [];\n this.isActionInProgress = false;\n }\n\n /**\n * Push a new action to the queue, and try to run it\n *\n * @param action Action object\n */\n public push(action: NotifierAction): void {\n this.actionQueue.push(action);\n this.tryToRunNextAction();\n }\n\n /**\n * Continue with the next action (called when the current action is finished)\n */\n public continue(): void {\n this.isActionInProgress = false;\n this.tryToRunNextAction();\n }\n\n /**\n * Try to run the next action in the queue; we skip if there already is some action in progress, or if there is no action left\n */\n private tryToRunNextAction(): void {\n if (this.isActionInProgress || this.actionQueue.length === 0) {\n return; // Skip (the queue can now go drink a coffee as it has nothing to do anymore)\n }\n this.isActionInProgress = true;\n this.actionStream.next(this.actionQueue.shift()); // Push next action to the stream, and remove the current action from the queue\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { Observable } from 'rxjs';\n\nimport { NotifierAction } from '../models/notifier-action.model';\nimport { NotifierConfig } from '../models/notifier-config.model';\nimport { NotifierNotificationOptions } from '../models/notifier-notification.model';\nimport { NotifierConfigToken } from '../notifier.tokens';\nimport { NotifierQueueService } from './notifier-queue.service';\n\n/**\n * Notifier service\n *\n * This service provides access to the public notifier API. Once injected into a component, directive, pipe, service, or any other building\n * block of an applications, it can be used to show new notifications, and hide existing ones. Internally, it transforms API calls into\n * actions, which then get thrown into the action queue - eventually being processed at the right moment.\n */\n@Injectable()\nexport class NotifierService {\n /**\n * Notifier queue service\n */\n private readonly queueService = inject(NotifierQueueService);\n\n /**\n * Notifier configuration\n */\n private readonly config = inject(NotifierConfigToken);\n\n /**\n * Get the notifier configuration\n *\n * @returns Notifier configuration\n */\n public getConfig(): NotifierConfig {\n return this.config;\n }\n\n /**\n * Get the observable for handling actions\n *\n * @returns Observable of NotifierAction\n */\n public get actionStream(): Observable<NotifierAction> {\n return this.queueService.actionStream.asObservable();\n }\n\n /**\n * API: Show a new notification\n *\n * @param notificationOptions Notification options\n */\n public show(notificationOptions: NotifierNotificationOptions): void {\n this.queueService.push({\n payload: notificationOptions,\n type: 'SHOW',\n });\n }\n\n /**\n * API: Hide a specific notification, given its ID\n *\n * @param notificationId ID of the notification to hide\n */\n public hide(notificationId: string): void {\n this.queueService.push({\n payload: notificationId,\n type: 'HIDE',\n });\n }\n\n /**\n * API: Hide the newest notification\n */\n public hideNewest(): void {\n this.queueService.push({\n type: 'HIDE_NEWEST',\n });\n }\n\n /**\n * API: Hide the oldest notification\n */\n public hideOldest(): void {\n this.queueService.push({\n type: 'HIDE_OLDEST',\n });\n }\n\n /**\n * API: Hide all notifications at once\n */\n public hideAll(): void {\n this.queueService.push({\n type: 'HIDE_ALL',\n });\n }\n\n /**\n * API: Shortcut for showing a new notification\n *\n * @param type Type of the notification\n * @param message Message of the notification\n * @param [notificationId] Unique ID for the notification (optional)\n */\n public notify(type: string, message: string, notificationId?: string): void {\n const notificationOptions: NotifierNotificationOptions = {\n message,\n type,\n };\n if (notificationId !== undefined) {\n notificationOptions.id = notificationId;\n }\n this.show(notificationOptions);\n }\n}\n","import { NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';\n\n/**\n * Fade animation preset\n */\nexport const fade: NotifierAnimationPreset = {\n hide: (): NotifierAnimationPresetKeyframes => {\n return {\n from: {\n opacity: '1',\n },\n to: {\n opacity: '0',\n },\n };\n },\n show: (): NotifierAnimationPresetKeyframes => {\n return {\n from: {\n opacity: '0',\n },\n to: {\n opacity: '1',\n },\n };\n },\n};\n","import { NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';\nimport { NotifierConfig } from '../models/notifier-config.model';\nimport { NotifierNotification } from '../models/notifier-notification.model';\n\n/**\n * Slide animation preset\n */\nexport const slide: NotifierAnimationPreset = {\n hide: (notification: NotifierNotification): NotifierAnimationPresetKeyframes => {\n // Prepare variables\n const config: NotifierConfig = notification.component.getConfig();\n const shift: number = notification.component.getShift();\n let from: {\n [animatablePropertyName: string]: string;\n };\n let to: {\n [animatablePropertyName: string]: string;\n };\n\n // Configure variables, depending on configuration and component\n if (config.position.horizontal.position === 'left') {\n from = {\n transform: `translate3d( 0, ${shift}px, 0 )`,\n };\n to = {\n transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), ${shift}px, 0 )`,\n };\n } else if (config.position.horizontal.position === 'right') {\n from = {\n transform: `translate3d( 0, ${shift}px, 0 )`,\n };\n to = {\n transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), ${shift}px, 0 )`,\n };\n } else {\n let horizontalPosition: string;\n if (config.position.vertical.position === 'top') {\n horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`;\n } else {\n horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`;\n }\n from = {\n transform: `translate3d( -50%, ${shift}px, 0 )`,\n };\n to = {\n transform: `translate3d( -50%, ${horizontalPosition}, 0 )`,\n };\n }\n\n // Done\n return {\n from,\n to,\n };\n },\n show: (notification: NotifierNotification): NotifierAnimationPresetKeyframes => {\n // Prepare variables\n const config: NotifierConfig = notification.component.getConfig();\n let from: {\n [animatablePropertyName: string]: string;\n };\n let to: {\n [animatablePropertyName: string]: string;\n };\n\n // Configure variables, depending on configuration and component\n if (config.position.horizontal.position === 'left') {\n from = {\n transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), 0, 0 )`,\n };\n to = {\n transform: 'translate3d( 0, 0, 0 )',\n };\n } else if (config.position.horizontal.position === 'right') {\n from = {\n transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), 0, 0 )`,\n };\n to = {\n transform: 'translate3d( 0, 0, 0 )',\n };\n } else {\n let horizontalPosition: string;\n if (config.position.vertical.position === 'top') {\n horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`;\n } else {\n horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`;\n }\n from = {\n transform: `translate3d( -50%, ${horizontalPosition}, 0 )`,\n };\n to = {\n transform: 'translate3d( -50%, 0, 0 )',\n };\n }\n\n // Done\n return {\n from,\n to,\n };\n },\n};\n","import { Injectable } from '@angular/core';\n\nimport { fade } from '../animation-presets/fade.animation-preset';\nimport { slide } from '../animation-presets/slide.animation-preset';\nimport { NotifierAnimationData, NotifierAnimationPreset, NotifierAnimationPresetKeyframes } from '../models/notifier-animation.model';\nimport { NotifierNotification } from '../models/notifier-notification.model';\n\n/**\n * Notifier animation service\n */\n@Injectable()\nexport class NotifierAnimationService {\n /**\n * List of animation presets (currently static)\n */\n private readonly animationPresets: {\n [animationPresetName: string]: NotifierAnimationPreset;\n };\n\n /**\n * Constructor\n */\n public constructor() {\n this.animationPresets = {\n fade,\n slide,\n };\n }\n\n /**\n * Get animation data\n *\n * This method generates all data the Web Animations API needs to animate our notification. The result depends on both the animation\n * direction (either in or out) as well as the notifications (and its attributes) itself.\n *\n * @param direction Animation direction, either in or out\n * @param notification Notification the animation data should be generated for\n * @returns Animation information\n */\n public getAnimationData(direction: 'show' | 'hide', notification: NotifierNotification): NotifierAnimationData {\n // Get all necessary animation data\n let keyframes: NotifierAnimationPresetKeyframes;\n let duration: number;\n let easing: string;\n if (direction === 'show') {\n keyframes = this.animationPresets[notification.component.getConfig().animations.show.preset].show(notification);\n duration = notification.component.getConfig().animations.show.speed;\n easing = notification.component.getConfig().animations.show.easing;\n } else {\n keyframes = this.animationPresets[notification.component.getConfig().animations.hide.preset].hide(notification);\n duration = notification.component.getConfig().animations.hide.speed;\n easing = notification.component.getConfig().animations.hide.easing;\n }\n\n // Build and return animation data\n return {\n keyframes: [keyframes.from, keyframes.to],\n options: {\n duration,\n easing,\n fill: 'forwards', // Keep the newly painted state after the animation finished\n },\n };\n }\n}\n","import { Injectable } from '@angular/core';\n\n/**\n * Notifier timer service\n *\n * This service acts as a timer, needed due to the still rather limited setTimeout JavaScript API. The timer service can start and stop a\n * timer. Furthermore, it can also pause the timer at any time, and resume later on. The timer API workd promise-based.\n */\n@Injectable()\nexport class NotifierTimerService {\n /**\n * Timestamp (in ms), created in the moment the timer starts\n */\n private now: number;\n\n /**\n * Remaining time (in ms)\n */\n private remaining: number;\n\n /**\n * Timeout ID, used for clearing the timeout later on\n */\n private timerId: number;\n\n /**\n * Promise resolve function, eventually getting called once the timer finishes\n */\n private finishPromiseResolver: () => void;\n\n /**\n * Constructor\n */\n public constructor() {\n this.now = 0;\n this.remaining = 0;\n }\n\n /**\n * Start (or resume) the timer\n *\n * @param duration Timer duration, in ms\n * @returns Promise, resolved once the timer finishes\n */\n public start(duration: number): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n // For the first run ...\n this.remaining = duration;\n\n // Setup, then start the timer\n this.finishPromiseResolver = resolve;\n this.continue();\n });\n }\n\n /**\n * Pause the timer\n */\n public pause(): void {\n clearTimeout(this.timerId);\n this.remaining -= new Date().getTime() - this.now;\n }\n\n /**\n * Continue the timer\n */\n public continue(): void {\n this.now = new Date().getTime();\n this.timerId = window.setTimeout(() => {\n this.finish();\n }, this.remaining);\n }\n\n /**\n * Stop the timer\n */\n public stop(): void {\n clearTimeout(this.timerId);\n this.remaining = 0;\n }\n\n /**\n * Finish up the timeout by resolving the timer promise\n */\n private finish(): void {\n this.finishPromiseResolver();\n }\n}\n","import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, Renderer2 } from '@angular/core';\n\nimport { NotifierAnimationData } from '../models/notifier-animation.model';\nimport { NotifierConfig } from '../models/notifier-config.model';\nimport { NotifierNotification } from '../models/notifier-notification.model';\nimport { NotifierService } from '../services/notifier.service';\nimport { NotifierAnimationService } from '../services/notifier-animation.service';\nimport { NotifierTimerService } from '../services/notifier-timer.service';\n\n/**\n * Notifier notification component\n * -------------------------------\n * This component is responsible for actually displaying the notification on screen. In addition, it's able to show and hide this\n * notification, in particular to animate this notification in and out, as well as shift (move) this notification vertically around.\n * Furthermore, the notification component handles all interactions the user has with this notification / component, such as clicks and\n * mouse movements.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters)\n host: {\n '(click)': 'onNotificationClick()',\n '(mouseout)': 'onNotificationMouseout()',\n '(mouseover)': 'onNotificationMouseover()',\n class: 'notifier__notification',\n },\n providers: [\n // We provide the timer to the component's local injector, so that every notification components gets its own\n // instance of the timer service, thus running their timers independently from each other\n NotifierTimerService,\n ],\n selector: 'notifier-notification',\n templateUrl: './notifier-notification.component.html',\n standalone: false,\n})\nexport class NotifierNotificationComponent implements AfterViewInit {\n /**\n * Input: Notification object, contains all details necessary to construct the notification\n */\n @Input()\n public notification: NotifierNotification;\n\n /**\n * Output: Ready event, handles the initialization success by emitting a reference to this notification component\n */\n @Output()\n public ready: EventEmitter<NotifierNotificationComponent>;\n\n /**\n * Output: Dismiss event, handles the click on the dismiss button by emitting the notification ID of this notification component\n */\n @Output()\n public dismiss: EventEmitter<string>;\n\n /**\n * Notifier configuration\n */\n public readonly config: NotifierConfig;\n\n /**\n * Notifier timer service\n */\n private readonly timerService: NotifierTimerService;\n\n /**\n * Notifier animation service\n */\n private readonly animationService: NotifierAnimationService;\n\n /**\n * Angular renderer, used to preserve the overall DOM abstraction & independence\n */\n private readonly renderer: Renderer2;\n\n /**\n * Native element reference, used for manipulating DOM properties\n */\n private readonly element: HTMLElement;\n\n /**\n * Current notification height, calculated and cached here (#perfmatters)\n */\n private elementHeight: number;\n\n /**\n * Current notification width, calculated and cached here (#perfmatters)\n */\n private elementWidth: number;\n\n /**\n * Current notification shift, calculated and cached here (#perfmatters)\n */\n private elementShift: number;\n\n /**\n * Constructor\n *\n * @param elementRef Reference to the component's element\n * @param renderer Angular renderer\n * @param notifierService Notifier service\n * @param notifierTimerService Notifier timer service\n * @param notifierAnimationService Notifier animation service\n */\n public constructor(\n elementRef: ElementRef,\n renderer: Renderer2,\n notifierService: NotifierService,\n notifierTimerService: NotifierTimerService,\n notifierAnimationService: NotifierAnimationService,\n ) {\n this.config = notifierService.getConfig();\n this.ready = new EventEmitter<NotifierNotificationComponent>();\n this.dismiss = new EventEmitter<string>();\n this.timerService = notifierTimerService;\n this.animationService = notifierAnimationService;\n this.renderer = renderer;\n this.element = elementRef.nativeElement;\n this.elementShift = 0;\n }\n\n /**\n * Component after view init lifecycle hook, setts up the component and then emits the ready event\n */\n public ngAfterViewInit(): void {\n this.setup();\n this.elementHeight = this.element.offsetHeight;\n this.elementWidth = this.element.offsetWidth;\n this.ready.emit(this);\n }\n\n /**\n * Get the notifier config\n *\n * @returns Notifier configuration\n */\n public getConfig(): NotifierConfig {\n return this.config;\n }\n\n /**\n * Get notification element height (in px)\n *\n * @returns Notification element height (in px)\n */\n public getHeight(): number {\n return this.elementHeight;\n }\n\n /**\n * Get notification element width (in px)\n *\n * @returns Notification element height (in px)\n */\n public getWidth(): number {\n return this.elementWidth;\n }\n\n /**\n * Get notification shift offset (in px)\n *\n * @returns Notification element shift offset (in px)\n */\n public getShift(): number {\n return this.elementShift;\n }\n\n /**\n * Show (animate in) this notification\n *\n * @returns Promise, resolved when done\n */\n public show(): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n // Are animations enabled?\n if (this.config.animations.enabled && this.config.animations.show.speed > 0) {\n // Get animation data\n const animationData: NotifierAnimationData = this.animationService.getAnimationData('show', this.notification);\n\n // Set initial styles (styles before animation), prevents quick flicker when animation starts\n const animatedProperties: Array<string> = Object.keys(animationData.keyframes[0]);\n for (let i: number = animatedProperties.length - 1; i >= 0; i--) {\n this.renderer.setStyle(this.element, animatedProperties[i], animationData.keyframes[0][animatedProperties[i]]);\n }\n\n // Animate notification in\n this.renderer.setStyle(this.element, 'visibility', 'visible');\n const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);\n animation.onfinish = () => {\n this.startAutoHideTimer();\n resolve(); // Done\n };\n } else {\n // Show notification\n this.renderer.setStyle(this.element, 'visibility', 'visible');\n this.startAutoHideTimer();\n resolve(); // Done\n }\n });\n }\n\n /**\n * Hide (animate out) this notification\n *\n * @returns Promise, resolved when done\n */\n public hide(): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n this.stopAutoHideTimer();\n\n // Are animations enabled?\n if (this.config.animations.enabled && this.config.animations.hide.speed > 0) {\n const animationData: NotifierAnimationData = this.animationService.getAnimationData('hide', this.notification);\n const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);\n animation.onfinish = () => {\n resolve(); // Done\n };\n } else {\n resolve(); // Done\n }\n });\n }\n\n /**\n * Shift (move) this notification\n *\n * @param distance Distance to shift (in px)\n * @param shiftToMakePlace Flag, defining in which direction to shift\n * @returns Promise, resolved when done\n */\n public shift(distance: number, shiftToMakePlace: boolean): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n // Calculate new position (position after the shift)\n let newElementShift: number;\n if (\n (this.config.position.vertical.position === 'top' && shiftToMakePlace) ||\n (this.config.position.vertical.position === 'bottom' && !shiftToMakePlace)\n ) {\n newElementShift = this.elementShift + distance + this.config.position.vertical.gap;\n } else {\n newElementShift = this.elementShift - distance - this.config.position.vertical.gap;\n }\n const horizontalPosition: string = this.config.position.horizontal.position === 'middle' ? '-50%' : '0';\n\n // Are animations enabled?\n if (this.config.animations.enabled && this.config.animations.shift.speed > 0) {\n const animationData: NotifierAnimationData = {\n // TODO: Extract into animation service\n keyframes: [\n {\n transform: `translate3d( ${horizontalPosition}, ${this.elementShift}px, 0 )`,\n },\n {\n transform: `translate3d( ${horizontalPosition}, ${newElementShift}px, 0 )`,\n },\n ],\n options: {\n duration: this.config.animations.shift.speed,\n easing: this.config.animations.shift.easing,\n fill: 'forwards',\n },\n };\n this.elementShift = newElementShift;\n const animation: Animation = this.element.animate(animationData.keyframes, animationData.options);\n animation.onfinish = () => {\n resolve(); // Done\n };\n } else {\n this.renderer.setStyle(this.element, 'transform', `translate3d( ${horizontalPosition}, ${newElementShift}px, 0 )`);\n this.elementShift = newElementShift;\n resolve(); // Done\n }\n });\n }\n\n /**\n * Handle click on dismiss button\n */\n public onClickDismiss(): void {\n this.dismiss.emit(this.notification.id);\n }\n\n /**\n * Handle mouseover over notification area\n */\n public onNotificationMouseover(): void {\n if (this.config.behaviour.onMouseover === 'pauseAutoHide') {\n this.pauseAutoHideTimer();\n } else if (this.config.behaviour.onMouseover === 'resetAutoHide') {\n this.stopAutoHideTimer();\n }\n }\n\n /**\n * Handle mouseout from notification area\n */\n public onNotificationMouseout(): void {\n if (this.config.behaviour.onMouseover === 'pauseAutoHide') {\n this.continueAutoHideTimer();\n } else if (this.config.behaviour.onMouseover === 'resetAutoHide') {\n this.startAutoHideTimer();\n }\n }\n\n /**\n * Handle click on notification area\n */\n public onNotificationClick(): void {\n if (this.config.behaviour.onClick === 'hide') {\n this.onClickDismiss();\n }\n }\n\n /**\n * Start the auto hide timer (if enabled)\n */\n private startAutoHideTimer(): void {\n if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {\n this.timerService.start(this.config.behaviour.autoHide).then(() => {\n this.onClickDismiss();\n });\n }\n }\n\n /**\n * Pause the auto hide timer (if enabled)\n */\n private pauseAutoHideTimer(): void {\n if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {\n this.timerService.pause();\n }\n }\n\n /**\n * Continue the auto hide timer (if enabled)\n */\n private continueAutoHideTimer(): void {\n if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {\n this.timerService.continue();\n }\n }\n\n /**\n * Stop the auto hide timer (if enabled)\n */\n private stopAutoHideTimer(): void {\n if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) {\n this.timerService.stop();\n }\n }\n\n /**\n * Initial notification setup\n */\n private setup(): void {\n // Set start position (initially the exact same for every new notification)\n if (this.config.position.horizontal.position === 'left') {\n this.renderer.setStyle(this.element, 'left', `${this.config.position.horizontal.distance}px`);\n } else if (this.config.position.horizontal.position === 'right') {\n this.renderer.setStyle(this.element, 'right', `${this.config.position.horizontal.distance}px`);\n } else {\n this.renderer.setStyle(this.element, 'left', '50%');\n // Let's get the GPU handle some work as well (#perfmatters)\n this.renderer.setStyle(this.element, 'transform', 'translate3d( -50%, 0, 0 )');\n }\n if (this.config.position.vertical.position === 'top') {\n this.renderer.setStyle(this.element, 'top', `${this.config.position.vertical.distance}px`);\n } else {\n this.renderer.setStyle(this.element, 'bottom', `${this.config.position.vertical.distance}px`);\n }\n\n // Add classes (responsible for visual design)\n this.renderer.addClass(this.element, `notifier__notification--${this.notification.type}`);\n this.renderer.addClass(this.element, `notifier__notification--${this.config.theme}`);\n }\n}\n","@if (notification.template) {\n <ng-container\n [ngTemplateOutlet]=\"notification.template\"\n [ngTemplateOutletContext]=\"{ notification: notification }\"\n >\n </ng-container>\n} @else {\n <p class=\"notifier__notification-message\">{{ notification.message }}</p>\n @if (config.behaviour.showDismissButton) {\n <button\n class=\"notifier__notification-button\"\n type=\"button\"\n title=\"dismiss\"\n (click)=\"onClickDismiss()\"\n >\n <svg class=\"notifier__notification-button-icon\" viewBox=\"0 0 24 24\" width=\"20\" height=\"20\">\n <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" />\n </svg>\n </button>\n }\n}\n\n","import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';\nimport { Subscription } from 'rxjs';\n\nimport { NotifierAction } from '../models/notifier-action.model';\nimport { NotifierConfig } from '../models/notifier-config.model';\nimport { NotifierNotification } from '../models/notifier-notification.model';\nimport { NotifierService } from '../services/notifier.service';\nimport { NotifierQueueService } from '../services/notifier-queue.service';\nimport { NotifierNotificationComponent } from './notifier-notification.component';\n\n/**\n * Notifier container component\n * ----------------------------\n * This component acts as a wrapper for all notification components; consequently, it is responsible for creating a new notification\n * component and removing an existing notification component. Being more precicely, it also handles side effects of those actions, such as\n * shifting or even completely removing other notifications as well. Overall, this components handles actions coming from the queue service\n * by subscribing to its action stream.\n *\n * Technical sidenote:\n * This component has to be used somewhere in an application to work; it will not inject and create itself automatically, primarily in order\n * to not break the Angular AoT compilation. Moreover, this component (and also the notification components) set their change detection\n * strategy onPush, which means that we handle change detection manually in order to get the best performance. (#perfmatters)\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush, // (#perfmatters)\n host: {\n class: 'notifier__container',\n },\n selector: 'notifier-container',\n templateUrl: './notifier-container.component.html',\n standalone: false,\n})\nexport class NotifierContainerComponent implements OnDestroy {\n /**\n * List of currently somewhat active notifications\n */\n public notifications: Array<NotifierNotification>;\n\n /**\n * Change detector\n */\n private readonly changeDetector: ChangeDetectorRef;\n\n /**\n * Notifier queue service\n */\n private readonly queueService: NotifierQueueService;\n\n /**\n * Notifier configuration\n */\n private readonly config: NotifierConfig;\n\n /**\n * Queue service observable subscription (saved for cleanup)\n */\n private queueServiceSubscription: Subscription;\n\n /**\n * Promise resolve function reference, temporarily used while the notification child component gets created\n */\n private tempPromiseResolver: () => void;\n\n /**\n * Constructor\n *\n * @param changeDetector Change detector, used for manually triggering change detection runs\n * @param notifierQueueService Notifier queue service\n * @param notifierService Notifier service\n */\n public constructor(changeDetector: ChangeDetectorRef, notifierQueueService: NotifierQueueService, notifierService: NotifierService) {\n this.changeDetector = changeDetector;\n this.queueService = notifierQueueService;\n this.config = notifierService.getConfig();\n this.notifications = [];\n\n // Connects this component up to the action queue, then handle incoming actions\n this.queueServiceSubscription = this.queueService.actionStream.subscribe((action: NotifierAction) => {\n this.handleAction(action).then(() => {\n this.queueService.continue();\n });\n });\n }\n\n /**\n * Component destroyment lifecycle hook, cleans up the observable subsciption\n */\n public ngOnDestroy(): void {\n if (this.queueServiceSubscription) {\n this.queueServiceSubscription.unsubscribe();\n }\n }\n\n /**\n * Notification identifier, used as the ngFor trackby function\n *\n * @param index Index\n * @param notification Notifier notification\n * @returns Notification ID as the unique identnfier\n */\n public identifyNotification(index: number, notification: NotifierNotification): string {\n return notification.id;\n }\n\n /**\n * Event handler, handles clicks on notification dismiss buttons\n *\n * @param notificationId ID of the notification to dismiss\n */\n public onNotificationDismiss(notificationId: string): void {\n this.queueService.push({\n payload: notificationId,\n type: 'HIDE',\n });\n }\n\n /**\n * Event handler, handles notification ready events\n *\n * @param notificationComponent Notification component reference\n */\n public onNotificationReady(notificationComponent: NotifierNotificationComponent): void {\n const currentNotification: NotifierNotification = this.notifications[this.notifications.length - 1]; // Get the latest notification\n currentNotification.component = notificationComponent; // Save the new omponent reference\n this.continueHandleShowAction(currentNotification); // Continue with handling the show action\n }\n\n /**\n * Handle incoming actions by mapping action types to methods, and then running them\n *\n * @param action Action object\n * @returns Promise, resolved when done\n */\n private handleAction(action: NotifierAction): Promise<void> {\n switch (\n action.type // TODO: Maybe a map (actionType -> class method) is a cleaner solution here?\n ) {\n case 'SHOW':\n return this.handleShowAction(action);\n case 'HIDE':\n return this.handleHideAction(action);\n case 'HIDE_OLDEST':\n return this.handleHideOldestAction(action);\n case 'HIDE_NEWEST':\n return this.handleHideNewestAction(action);\n case 'HIDE_ALL':\n return this.handleHideAllAction();\n default:\n return new Promise<void>((resolve: () => void) => {\n resolve(); // Ignore unknown action types\n });\n }\n }\n\n /**\n * Show a new notification\n *\n * We simply add the notification to the list, and then wait until its properly initialized / created / rendered.\n *\n * @param action Action object\n * @returns Promise, resolved when done\n */\n private handleShowAction(action: NotifierAction): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n this.tempPromiseResolver = resolve; // Save the promise resolve function so that it can be called later on by another method\n this.addNotificationToList(new NotifierNotification(action.payload));\n });\n }\n\n /**\n * Continue to show a new notification (after the notification components is initialized / created / rendered).\n *\n * If this is the first (and thus only) notification, we can simply show it. Otherwhise, if stacking is disabled (or a low value), we\n * switch out notifications, in particular we hide the existing one, and then show our new one. Yet, if stacking is enabled, we first\n * shift all older notifications, and then show our new notification. In addition, if there are too many notification on the screen,\n * we hide the oldest one first. Furthermore, if configured, animation overlapping is applied.\n *\n * @param notification New notification to show\n */\n private continueHandleShowAction(notification: NotifierNotification): void {\n // First (which means only one) notification in the list?\n const numberOfNotifications: number = this.notifications.length;\n if (numberOfNotifications === 1) {\n notification.component.show().then(this.tempPromiseResolver); // Done\n } else {\n const implicitStackingLimit = 2;\n\n // Stacking enabled? (stacking value below 2 means stacking is disabled)\n if (this.config.behaviour.stacking === false || this.config.behaviour.stacking < implicitStackingLimit) {\n this.notifications[0].component.hide().then(() => {\n this.removeNotificationFromList(this.notifications[0]);\n notification.component.show().then(this.tempPromiseResolver); // Done\n });\n } else {\n const stepPromises: Array<Promise<void>> = [];\n\n // Are there now too many notifications?\n if (numberOfNotifications > this.config.behaviour.stacking) {\n const oldNotifications: Array<NotifierNotification> = this.notifications.slice(1, numberOfNotifications - 1);\n\n // Are animations enabled?\n if (this.config.animations.enabled) {\n // Is animation overlap enabled?\n if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {\n stepPromises.push(this.notifications[0].component.hide());\n setTimeout(() => {\n stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));\n }, this.config.animations.hide.speed - this.config.animations.overlap);\n setTimeout(\n () => {\n stepPromises.push(notification.component.show());\n },\n this.config.animations.hide.speed + this.config.animations.shift.speed - this.config.animations.overlap,\n );\n } else {\n stepPromises.push(\n new Promise<void>((resolve: () => void) => {\n this.notifications[0].component.hide().then(() => {\n this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => {\n notification.component.show().then(resolve);\n });\n });\n }),\n );\n }\n } else {\n stepPromises.push(this.notifications[0].component.hide());\n stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));\n stepPromises.push(notification.component.show());\n }\n } else {\n const oldNotifications: Array<NotifierNotification> = this.notifications.slice(0, numberOfNotifications - 1);\n\n // Are animations enabled?\n if (this.config.animations.enabled) {\n // Is animation overlap enabled?\n if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {\n stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));\n setTimeout(() => {\n stepPromises.push(notification.component.show());\n }, this.config.animations.shift.speed - this.config.animations.overlap);\n } else {\n stepPromises.push(\n new Promise<void>((resolve: () => void) => {\n this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => {\n notification.component.show().then(resolve);\n });\n }),\n );\n }\n } else {\n stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true));\n stepPromises.push(notification.component.show());\n }\n }\n\n Promise.all(stepPromises).then(() => {\n if (typeof this.config.behaviour.stacking == 'number' && numberOfNotifications > this.config.behaviour.stacking) {\n this.removeNotificationFromList(this.notifications[0]);\n }\n this.tempPromiseResolver();\n }); // Done\n }\n }\n }\n\n /**\n * Hide an existing notification\n *\n * Fist, we skip everything if there are no notifications at all, or the given notification does not exist. Then, we hide the given\n * notification. If there exist older notifications, we then shift them around to fill the gap. Once both hiding the given notification\n * and shifting the older notificaitons is done, the given notification gets finally removed (from the DOM).\n *\n * @param action Action object, payload contains the notification ID\n * @returns Promise, resolved when done\n */\n private handleHideAction(action: NotifierAction): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n const stepPromises: Array<Promise<void>> = [];\n\n // Does the notification exist / are there even any notifications? (let's prevent accidential errors)\n const notification: NotifierNotification | undefined = this.findNotificationById(action.payload);\n if (notification === undefined) {\n resolve();\n return;\n }\n\n // Get older notifications\n const notificationIndex: number | undefined = this.findNotificationIndexById(action.payload);\n if (notificationIndex === undefined) {\n resolve();\n return;\n }\n const oldNotifications: Array<NotifierNotification> = this.notifications.slice(0, notificationIndex);\n\n // Do older notifications exist, and thus do we need to shift other notifications as a consequence?\n if (oldNotifications.length > 0) {\n // Are animations enabled?\n if (this.config.animations.enabled && this.config.animations.hide.speed > 0) {\n // Is animation overlap enabled?\n if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) {\n stepPromises.push(notification.component.hide());\n setTimeout(() => {\n stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));\n }, this.config.animations.hide.speed - this.config.animations.overlap);\n } else {\n notification.component.hide().then(() => {\n stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));\n });\n }\n } else {\n stepPromises.push(notification.component.hide());\n stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false));\n }\n } else {\n stepPromises.push(notification.component.hide());\n }\n\n // Wait until both hiding and shifting is done, then remove the notification from the list\n Promise.all(stepPromises).then(() => {\n this.removeNotificationFromList(notification);\n resolve(); // Done\n });\n });\n }\n\n /**\n * Hide the oldest notification (bridge to handleHideAction)\n *\n * @param action Action object\n * @returns Promise, resolved when done\n */\n private handleHideOldestAction(action: NotifierAction): Promise<void> {\n // Are there any notifications? (prevent accidential errors)\n if (this.notifications.length === 0) {\n return new Promise<void>((resolve: () => void) => {\n resolve();\n }); // Done\n } else {\n action.payload = this.notifications[0].id;\n return this.handleHideAction(action);\n }\n }\n\n /**\n * Hide the newest notification (bridge to handleHideAction)\n *\n * @param action Action object\n * @returns Promise, resolved when done\n */\n private handleHideNewestAction(action: NotifierAction): Promise<void> {\n // Are there any notifications? (prevent accidential errors)\n if (this.notifications.length === 0) {\n return new Promise<void>((resolve: () => void) => {\n resolve();\n }); // Done\n } else {\n action.payload = this.notifications[this.notifications.length - 1].id;\n return this.handleHideAction(action);\n }\n }\n\n /**\n * Hide all notifications at once\n *\n * @returns Promise, resolved when done\n */\n private handleHideAllAction(): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n // Are there any notifications? (prevent accidential errors)\n const numberOfNotifications: number = this.notifications.length;\n if (numberOfNotifications === 0) {\n resolve(); // Done\n return;\n }\n\n // Are animations enabled?\n if (\n this.config.animations.enabled &&\n this.config.animations.hide.speed > 0 &&\n this.config.animations.hide.offset !== false &&\n this.config.animations.hide.offset > 0\n ) {\n for (let i: number = numberOfNotifications - 1; i >= 0; i--) {\n const animationOffset: number = this.config.position.vertical.position === 'top' ? numberOfNotifications - 1 : i;\n setTimeout(() => {\n this.notifications[i].component.hide().then(() => {\n // Are we done here, was this the last notification to be hidden?\n if (\n (this.config.position.vertical.position === 'top' && i === 0) ||\n (this.config.position.vertical.position === 'bottom' && i === numberOfNotifications - 1)\n ) {\n this.removeAllNotificationsFromList();\n resolve(); // Done\n }\n });\n }, this.config.animations.hide.offset * animationOffset);\n }\n } else {\n const stepPromises: Array<Promise<void>> = [];\n for (let i: number = numberOfNotifications - 1; i >= 0; i--) {\n stepPromises.push(this.notifications[i].component.hide());\n }\n Promise.all(stepPromises).then(() => {\n this.removeAllNotificationsFromList();\n resolve(); // Done\n });\n }\n });\n }\n\n /**\n * Shift multiple notifications at once\n *\n * @param notifications List containing the notifications to be shifted\n * @param distance Distance to shift (in px)\n * @param toMakePlace Flag, defining in which direciton to shift\n * @returns Promise, resolved when done\n */\n private shiftNotifications(notifications: Array<NotifierNotification>, distance: number, toMakePlace: boolean): Promise<void> {\n return new Promise<void>((resolve: () => void) => {\n // Are there any notifications to shift?\n if (notifications.length === 0) {\n resolve();\n return;\n }\n\n const notificationPromises: Array<Promise<void>> = [];\n for (let i: number = notifications.length - 1; i >= 0; i--) {\n notificationPromises.push(notifications[i].component.shift(distance, toMakePlace));\n }\n Promise.all(notificationPromises).then(resolve); // Done\n });\n }\n\n /**\n * Add a new notification to the list of notifications (t