UNPKG

gramli-angular-notifier

Version:

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

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