UNPKG

gramli-angular-notifier

Version:

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

1,141 lines (1,131 loc) 59 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, inject, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core'; import { Subject } from 'rxjs'; import * as i4 from '@angular/common'; import { CommonModule } from '@angular/common'; /** * Notification * * This class describes the structure of a notifiction, including all information it needs to live, and everyone else needs to work with it. */ class NotifierNotification { /** * Constructor * * @param options Notifier options */ constructor(options) { /** * The template to customize * the appearance of the notification */ this.template = null; Object.assign(this, options); // If not set manually, we have to create a unique notification ID by ourselves. The ID generation relies on the current browser // datetime in ms, in praticular the moment this notification gets constructed. Concurrency, and thus two IDs being the exact same, // is not possible due to the action queue concept. if (options.id === undefined) { this.id = `ID_${new Date().getTime()}`; } } } /** * Injection Token for notifier options */ const NotifierOptionsToken = new InjectionToken('[angular-notifier] Notifier Options'); /** * Injection Token for notifier configuration */ const NotifierConfigToken = new InjectionToken('[anuglar-notifier] Notifier Config'); /** * Notifier queue service * * In general, API calls don't get processed right away. Instead, we have to queue them up in order to prevent simultanious API calls * interfering with each other. This, at least in theory, is possible at any time. In particular, animations - which potentially overlap - * 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 * processes them at the right time (which is when the previous action has been processed successfully). * * Technical sidenote: * An action looks pretty similar to the ones within the Flux / Redux pattern. */ class NotifierQueueService { /** * Constructor */ constructor() { this.actionStream = new Subject(); this.actionQueue = []; this.isActionInProgress = false; } /** * Push a new action to the queue, and try to run it * * @param action Action object */ push(action) { this.actionQueue.push(action); this.tryToRunNextAction(); } /** * Continue with the next action (called when the current action is finished) */ continue() { this.isActionInProgress = false; this.tryToRunNextAction(); } /** * 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 */ tryToRunNextAction() { if (this.isActionInProgress || this.actionQueue.length === 0) { return; // Skip (the queue can now go drink a coffee as it has nothing to do anymore) } this.isActionInProgress = true; this.actionStream.next(this.actionQueue.shift()); // Push next action to the stream, and remove the current action from the queue } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierQueueService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierQueueService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierQueueService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * Notifier service * * This service provides access to the public notifier API. Once injected into a component, directive, pipe, service, or any other building * block of an applications, it can be used to show new notifications, and hide existing ones. Internally, it transforms API calls into * actions, which then get thrown into the action queue - eventually being processed at the right moment. */ class NotifierService { constructor() { /** * Notifier queue service */ this.queueService = inject(NotifierQueueService); /** * Notifier configuration */ this.config = inject(NotifierConfigToken); } /** * Get the notifier configuration * * @returns Notifier configuration */ getConfig() { return this.config; } /** * Get the observable for handling actions * * @returns Observable of NotifierAction */ get actionStream() { return this.queueService.actionStream.asObservable(); } /** * API: Show a new notification * * @param notificationOptions Notification options */ show(notificationOptions) { this.queueService.push({ payload: notificationOptions, type: 'SHOW', }); } /** * API: Hide a specific notification, given its ID * * @param notificationId ID of the notification to hide */ hide(notificationId) { this.queueService.push({ payload: notificationId, type: 'HIDE', }); } /** * API: Hide the newest notification */ hideNewest() { this.queueService.push({ type: 'HIDE_NEWEST', }); } /** * API: Hide the oldest notification */ hideOldest() { this.queueService.push({ type: 'HIDE_OLDEST', }); } /** * API: Hide all notifications at once */ hideAll() { this.queueService.push({ type: 'HIDE_ALL', }); } /** * API: Shortcut for showing a new notification * * @param type Type of the notification * @param message Message of the notification * @param [notificationId] Unique ID for the notification (optional) */ notify(type, message, notificationId) { const notificationOptions = { message, type, }; if (notificationId !== undefined) { notificationOptions.id = notificationId; } this.show(notificationOptions); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierService, decorators: [{ type: Injectable }] }); /** * Fade animation preset */ const fade = { hide: () => { return { from: { opacity: '1', }, to: { opacity: '0', }, }; }, show: () => { return { from: { opacity: '0', }, to: { opacity: '1', }, }; }, }; /** * Slide animation preset */ const slide = { hide: (notification) => { // Prepare variables const config = notification.component.getConfig(); const shift = notification.component.getShift(); let from; let to; // Configure variables, depending on configuration and component if (config.position.horizontal.position === 'left') { from = { transform: `translate3d( 0, ${shift}px, 0 )`, }; to = { transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), ${shift}px, 0 )`, }; } else if (config.position.horizontal.position === 'right') { from = { transform: `translate3d( 0, ${shift}px, 0 )`, }; to = { transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), ${shift}px, 0 )`, }; } else { let horizontalPosition; if (config.position.vertical.position === 'top') { horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`; } else { horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`; } from = { transform: `translate3d( -50%, ${shift}px, 0 )`, }; to = { transform: `translate3d( -50%, ${horizontalPosition}, 0 )`, }; } // Done return { from, to, }; }, show: (notification) => { // Prepare variables const config = notification.component.getConfig(); let from; let to; // Configure variables, depending on configuration and component if (config.position.horizontal.position === 'left') { from = { transform: `translate3d( calc( -100% - ${config.position.horizontal.distance}px - 10px ), 0, 0 )`, }; to = { transform: 'translate3d( 0, 0, 0 )', }; } else if (config.position.horizontal.position === 'right') { from = { transform: `translate3d( calc( 100% + ${config.position.horizontal.distance}px + 10px ), 0, 0 )`, }; to = { transform: 'translate3d( 0, 0, 0 )', }; } else { let horizontalPosition; if (config.position.vertical.position === 'top') { horizontalPosition = `calc( -100% - ${config.position.horizontal.distance}px - 10px )`; } else { horizontalPosition = `calc( 100% + ${config.position.horizontal.distance}px + 10px )`; } from = { transform: `translate3d( -50%, ${horizontalPosition}, 0 )`, }; to = { transform: 'translate3d( -50%, 0, 0 )', }; } // Done return { from, to, }; }, }; /** * Notifier animation service */ class NotifierAnimationService { /** * Constructor */ constructor() { this.animationPresets = { fade, slide, }; } /** * Get animation data * * This method generates all data the Web Animations API needs to animate our notification. The result depends on both the animation * direction (either in or out) as well as the notifications (and its attributes) itself. * * @param direction Animation direction, either in or out * @param notification Notification the animation data should be generated for * @returns Animation information */ getAnimationData(direction, notification) { // Get all necessary animation data let keyframes; let duration; let easing; if (direction === 'show') { keyframes = this.animationPresets[notification.component.getConfig().animations.show.preset].show(notification); duration = notification.component.getConfig().animations.show.speed; easing = notification.component.getConfig().animations.show.easing; } else { keyframes = this.animationPresets[notification.component.getConfig().animations.hide.preset].hide(notification); duration = notification.component.getConfig().animations.hide.speed; easing = notification.component.getConfig().animations.hide.easing; } // Build and return animation data return { keyframes: [keyframes.from, keyframes.to], options: { duration, easing, fill: 'forwards', // Keep the newly painted state after the animation finished }, }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierAnimationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierAnimationService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierAnimationService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * Notifier timer service * * This service acts as a timer, needed due to the still rather limited setTimeout JavaScript API. The timer service can start and stop a * timer. Furthermore, it can also pause the timer at any time, and resume later on. The timer API workd promise-based. */ class NotifierTimerService { /** * Constructor */ constructor() { this.now = 0; this.remaining = 0; } /** * Start (or resume) the timer * * @param duration Timer duration, in ms * @returns Promise, resolved once the timer finishes */ start(duration) { return new Promise((resolve) => { // For the first run ... this.remaining = duration; // Setup, then start the timer this.finishPromiseResolver = resolve; this.continue(); }); } /** * Pause the timer */ pause() { clearTimeout(this.timerId); this.remaining -= new Date().getTime() - this.now; } /** * Continue the timer */ continue() { this.now = new Date().getTime(); this.timerId = window.setTimeout(() => { this.finish(); }, this.remaining); } /** * Stop the timer */ stop() { clearTimeout(this.timerId); this.remaining = 0; } /** * Finish up the timeout by resolving the timer promise */ finish() { this.finishPromiseResolver(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierTimerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierTimerService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierTimerService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /** * 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. */ class NotifierNotificationComponent { /** * 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 */ constructor(elementRef, renderer, notifierService, notifierTimerService, 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 */ ngAfterViewInit() { this.setup(); this.elementHeight = this.element.offsetHeight; this.elementWidth = this.element.offsetWidth; this.ready.emit(this); } /** * Get the notifier config * * @returns Notifier configuration */ getConfig() { return this.config; } /** * Get notification element height (in px) * * @returns Notification element height (in px) */ getHeight() { return this.elementHeight; } /** * Get notification element width (in px) * * @returns Notification element height (in px) */ getWidth() { return this.elementWidth; } /** * Get notification shift offset (in px) * * @returns Notification element shift offset (in px) */ getShift() { return this.elementShift; } /** * Show (animate in) this notification * * @returns Promise, resolved when done */ show() { return new Promise((resolve) => { // Are animations enabled? if (this.config.animations.enabled && this.config.animations.show.speed > 0) { // Get animation data const animationData = this.animationService.getAnimationData('show', this.notification); // Set initial styles (styles before animation), prevents quick flicker when animation starts const animatedProperties = Object.keys(animationData.keyframes[0]); for (let i = 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 = 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 */ hide() { return new Promise((resolve) => { this.stopAutoHideTimer(); // Are animations enabled? if (this.config.animations.enabled && this.config.animations.hide.speed > 0) { const animationData = this.animationService.getAnimationData('hide', this.notification); const 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 */ shift(distance, shiftToMakePlace) { return new Promise((resolve) => { // Calculate new position (position after the shift) let newElementShift; 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 = this.config.position.horizontal.position === 'middle' ? '-50%' : '0'; // Are animations enabled? if (this.config.animations.enabled && this.config.animations.shift.speed > 0) { const animationData = { // 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 = 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 */ onClickDismiss() { this.dismiss.emit(this.notification.id); } /** * Handle mouseover over notification area */ onNotificationMouseover() { if (this.config.behaviour.onMouseover === 'pauseAutoHide') { this.pauseAutoHideTimer(); } else if (this.config.behaviour.onMouseover === 'resetAutoHide') { this.stopAutoHideTimer(); } } /** * Handle mouseout from notification area */ onNotificationMouseout() { if (this.config.behaviour.onMouseover === 'pauseAutoHide') { this.continueAutoHideTimer(); } else if (this.config.behaviour.onMouseover === 'resetAutoHide') { this.startAutoHideTimer(); } } /** * Handle click on notification area */ onNotificationClick() { if (this.config.behaviour.onClick === 'hide') { this.onClickDismiss(); } } /** * Start the auto hide timer (if enabled) */ startAutoHideTimer() { 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) */ pauseAutoHideTimer() { if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) { this.timerService.pause(); } } /** * Continue the auto hide timer (if enabled) */ continueAutoHideTimer() { if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) { this.timerService.continue(); } } /** * Stop the auto hide timer (if enabled) */ stopAutoHideTimer() { if (this.config.behaviour.autoHide !== false && this.config.behaviour.autoHide > 0) { this.timerService.stop(); } } /** * Initial notification setup */ setup() { // 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}`); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierNotificationComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: NotifierService }, { token: NotifierTimerService }, { token: NotifierAnimationService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: NotifierNotificationComponent, isStandalone: false, selector: "notifier-notification", inputs: { notification: "notification" }, outputs: { ready: "ready", dismiss: "dismiss" }, host: { listeners: { "click": "onNotificationClick()", "mouseout": "onNotificationMouseout()", "mouseover": "onNotificationMouseover()" }, classAttribute: "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, ], ngImport: i0, template: "@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", dependencies: [{ kind: "directive", type: i4.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierNotificationComponent, decorators: [{ type: Component, args: [{ changeDetection: ChangeDetectionStrategy.OnPush, 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', standalone: false, template: "@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" }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: NotifierService }, { type: NotifierTimerService }, { type: NotifierAnimationService }], propDecorators: { notification: [{ type: Input }], ready: [{ type: Output }], dismiss: [{ type: Output }] } }); /** * Notifier container component * ---------------------------- * This component acts as a wrapper for all notification components; consequently, it is responsible for creating a new notification * component and removing an existing notification component. Being more precicely, it also handles side effects of those actions, such as * shifting or even completely removing other notifications as well. Overall, this components handles actions coming from the queue service * by subscribing to its action stream. * * Technical sidenote: * This component has to be used somewhere in an application to work; it will not inject and create itself automatically, primarily in order * to not break the Angular AoT compilation. Moreover, this component (and also the notification components) set their change detection * strategy onPush, which means that we handle change detection manually in order to get the best performance. (#perfmatters) */ class NotifierContainerComponent { /** * Constructor * * @param changeDetector Change detector, used for manually triggering change detection runs * @param notifierQueueService Notifier queue service * @param notifierService Notifier service */ constructor(changeDetector, notifierQueueService, notifierService) { this.changeDetector = changeDetector; this.queueService = notifierQueueService; this.config = notifierService.getConfig(); this.notifications = []; // Connects this component up to the action queue, then handle incoming actions this.queueServiceSubscription = this.queueService.actionStream.subscribe((action) => { this.handleAction(action).then(() => { this.queueService.continue(); }); }); } /** * Component destroyment lifecycle hook, cleans up the observable subsciption */ ngOnDestroy() { if (this.queueServiceSubscription) { this.queueServiceSubscription.unsubscribe(); } } /** * Notification identifier, used as the ngFor trackby function * * @param index Index * @param notification Notifier notification * @returns Notification ID as the unique identnfier */ identifyNotification(index, notification) { return notification.id; } /** * Event handler, handles clicks on notification dismiss buttons * * @param notificationId ID of the notification to dismiss */ onNotificationDismiss(notificationId) { this.queueService.push({ payload: notificationId, type: 'HIDE', }); } /** * Event handler, handles notification ready events * * @param notificationComponent Notification component reference */ onNotificationReady(notificationComponent) { const currentNotification = this.notifications[this.notifications.length - 1]; // Get the latest notification currentNotification.component = notificationComponent; // Save the new omponent reference this.continueHandleShowAction(currentNotification); // Continue with handling the show action } /** * Handle incoming actions by mapping action types to methods, and then running them * * @param action Action object * @returns Promise, resolved when done */ handleAction(action) { switch (action.type // TODO: Maybe a map (actionType -> class method) is a cleaner solution here? ) { case 'SHOW': return this.handleShowAction(action); case 'HIDE': return this.handleHideAction(action); case 'HIDE_OLDEST': return this.handleHideOldestAction(action); case 'HIDE_NEWEST': return this.handleHideNewestAction(action); case 'HIDE_ALL': return this.handleHideAllAction(); default: return new Promise((resolve) => { resolve(); // Ignore unknown action types }); } } /** * Show a new notification * * We simply add the notification to the list, and then wait until its properly initialized / created / rendered. * * @param action Action object * @returns Promise, resolved when done */ handleShowAction(action) { return new Promise((resolve) => { this.tempPromiseResolver = resolve; // Save the promise resolve function so that it can be called later on by another method this.addNotificationToList(new NotifierNotification(action.payload)); }); } /** * Continue to show a new notification (after the notification components is initialized / created / rendered). * * If this is the first (and thus only) notification, we can simply show it. Otherwhise, if stacking is disabled (or a low value), we * switch out notifications, in particular we hide the existing one, and then show our new one. Yet, if stacking is enabled, we first * shift all older notifications, and then show our new notification. In addition, if there are too many notification on the screen, * we hide the oldest one first. Furthermore, if configured, animation overlapping is applied. * * @param notification New notification to show */ continueHandleShowAction(notification) { // First (which means only one) notification in the list? const numberOfNotifications = this.notifications.length; if (numberOfNotifications === 1) { notification.component.show().then(this.tempPromiseResolver); // Done } else { const implicitStackingLimit = 2; // Stacking enabled? (stacking value below 2 means stacking is disabled) if (this.config.behaviour.stacking === false || this.config.behaviour.stacking < implicitStackingLimit) { this.notifications[0].component.hide().then(() => { this.removeNotificationFromList(this.notifications[0]); notification.component.show().then(this.tempPromiseResolver); // Done }); } else { const stepPromises = []; // Are there now too many notifications? if (numberOfNotifications > this.config.behaviour.stacking) { const oldNotifications = this.notifications.slice(1, numberOfNotifications - 1); // Are animations enabled? if (this.config.animations.enabled) { // Is animation overlap enabled? if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) { stepPromises.push(this.notifications[0].component.hide()); setTimeout(() => { stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true)); }, this.config.animations.hide.speed - this.config.animations.overlap); setTimeout(() => { stepPromises.push(notification.component.show()); }, this.config.animations.hide.speed + this.config.animations.shift.speed - this.config.animations.overlap); } else { stepPromises.push(new Promise((resolve) => { this.notifications[0].component.hide().then(() => { this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => { notification.component.show().then(resolve); }); }); })); } } else { stepPromises.push(this.notifications[0].component.hide()); stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true)); stepPromises.push(notification.component.show()); } } else { const oldNotifications = this.notifications.slice(0, numberOfNotifications - 1); // Are animations enabled? if (this.config.animations.enabled) { // Is animation overlap enabled? if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) { stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true)); setTimeout(() => { stepPromises.push(notification.component.show()); }, this.config.animations.shift.speed - this.config.animations.overlap); } else { stepPromises.push(new Promise((resolve) => { this.shiftNotifications(oldNotifications, notification.component.getHeight(), true).then(() => { notification.component.show().then(resolve); }); })); } } else { stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), true)); stepPromises.push(notification.component.show()); } } Promise.all(stepPromises).then(() => { if (typeof this.config.behaviour.stacking == 'number' && numberOfNotifications > this.config.behaviour.stacking) { this.removeNotificationFromList(this.notifications[0]); } this.tempPromiseResolver(); }); // Done } } } /** * Hide an existing notification * * Fist, we skip everything if there are no notifications at all, or the given notification does not exist. Then, we hide the given * notification. If there exist older notifications, we then shift them around to fill the gap. Once both hiding the given notification * and shifting the older notificaitons is done, the given notification gets finally removed (from the DOM). * * @param action Action object, payload contains the notification ID * @returns Promise, resolved when done */ handleHideAction(action) { return new Promise((resolve) => { const stepPromises = []; // Does the notification exist / are there even any notifications? (let's prevent accidential errors) const notification = this.findNotificationById(action.payload); if (notification === undefined) { resolve(); return; } // Get older notifications const notificationIndex = this.findNotificationIndexById(action.payload); if (notificationIndex === undefined) { resolve(); return; } const oldNotifications = this.notifications.slice(0, notificationIndex); // Do older notifications exist, and thus do we need to shift other notifications as a consequence? if (oldNotifications.length > 0) { // Are animations enabled? if (this.config.animations.enabled && this.config.animations.hide.speed > 0) { // Is animation overlap enabled? if (this.config.animations.overlap !== false && this.config.animations.overlap > 0) { stepPromises.push(notification.component.hide()); setTimeout(() => { stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false)); }, this.config.animations.hide.speed - this.config.animations.overlap); } else { notification.component.hide().then(() => { stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false)); }); } } else { stepPromises.push(notification.component.hide()); stepPromises.push(this.shiftNotifications(oldNotifications, notification.component.getHeight(), false)); } } else { stepPromises.push(notification.component.hide()); } // Wait until both hiding and shifting is done, then remove the notification from the list Promise.all(stepPromises).then(() => { this.removeNotificationFromList(notification); resolve(); // Done }); }); } /** * Hide the oldest notification (bridge to handleHideAction) * * @param action Action object * @returns Promise, resolved when done */ handleHideOldestAction(action) { // Are there any notifications? (prevent accidential errors) if (this.notifications.length === 0) { return new Promise((resolve) => { resolve(); }); // Done } else { action.payload = this.notifications[0].id; return this.handleHideAction(action); } } /** * Hide the newest notification (bridge to handleHideAction) * * @param action Action object * @returns Promise, resolved when done */ handleHideNewestAction(action) { // Are there any notifications? (prevent accidential errors) if (this.notifications.length === 0) { return new Promise((resolve) => { resolve(); }); // Done } else { action.payload = this.notifications[this.notifications.length - 1].id; return this.handleHideAction(action); } } /** * Hide all notifications at once * * @returns Promise, resolved when done */ handleHideAllAction() { return new Promise((resolve) => { // Are there any notifications? (prevent accidential errors) const numberOfNotifications = this.notifications.length; if (numberOfNotifications === 0) { resolve(); // Done return; } // Are animations enabled? if (this.config.animations.enabled && this.config.animations.hide.speed > 0 && this.config.animations.hide.offset !== false && this.config.animations.hide.offset > 0) { for (let i = numberOfNotifications - 1; i >= 0; i--) { const animationOffset = this.config.position.vertical.position === 'top' ? numberOfNotifications - 1 : i; setTimeout(() => { this.notifications[i].component.hide().then(() => { // Are we done here, was this the last notification to be hidden? if ((this.config.position.vertical.position === 'top' && i === 0) || (this.config.position.vertical.position === 'bottom' && i === numberOfNotifications - 1)) { this.removeAllNotificationsFromList(); resolve(); // Done } }); }, this.config.animations.hide.offset * animationOffset); } } else { const stepPromises = []; for (let i = numberOfNotifications - 1; i >= 0; i--) { stepPromises.push(this.notifications[i].component.hide()); } Promise.all(stepPromises).then(() => { this.removeAllNotificationsFromList(); resolve(); // Done }); } }); } /** * Shift multiple notifications at once * * @param notifications List containing the notifications to be shifted * @param distance Distance to shift (in px) * @param toMakePlace Flag, defining in which direciton to shift * @returns Promise, resolved when done */ shiftNotifications(notifications, distance, toMakePlace) { return new Promise((resolve) => { // Are there any notifications to shift? if (notifications.length === 0) { resolve(); return; } const notificationPromises = []; for (let i = notifications.length - 1; i >= 0; i--) { notificationPromises.push(notifications[i].component.shift(distance, toMakePlace)); } Promise.all(notificationPromises).then(resolve); // Done }); } /** * Add a new notification to the list of notifications (triggers change detection) * * @param notification Notification to add to the list of notifications */ addNotificationToList(notification) { this.notifications.push(notification); this.changeDetector.markForCheck(); // Run change detection because the notification list changed } /** * Remove an existing notification from the list of notifications (triggers change detection) * * @param notification Notification to be removed from the list of notifications */ removeNotificationFromList(notification) { this.notifications = this.notifications.filter((item) => item.component !== notification.component); this.changeDetector.markForCheck(); // Run change detection because the notification list changed } /** * Remove all notifications from the list (triggers change detection) */ removeAllNotificationsFromList() { this.notifications = []; this.changeDetector.markForCheck(); // Run change detection because the notification list changed } /** * Helper: Find a notification in the notification list by a given notification ID * * @param notificationId Notification ID, used for finding notification * @returns Notification, undefined if not found */ findNotificationById(notificationId) { return this.notifications.find((currentNotification) => currentNotification.id === notificationId); } /** * Helper: Find a notification's index by a given notification ID * * @param notificationId Notification ID, used for finding a notification's index * @returns Notification index, undefined if not found */ findNotificationIndexById(notificationId) { const notificationIndex = this.notifications.findIndex((currentNotification) => currentNotification.id === notificationId); return notificationIndex !== -1 ? notificationIndex : undefined; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: NotifierContainerComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: NotifierQueueService }, { token: NotifierService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.5", type: NotifierContainerComponent, isStandalone: false, selector: "notifier-container", host: { classAttribute: "notifier__container" }, ngImport: i0, template: "<ul class=\"notifier__container-list\">\n @for (notification of notifications; track identifyNotification($index, notification)) {\n <li class=\"notifier__container-list-item\">\n <notifier-notification [notification]=\"notification\" (ready)=\"onNotificationReady($event)\" (dismiss)=\"onNotificationDismiss($event)\">\n </notifier-notification>\n </li>\n }\n</ul>\n", dependencies: [{ kind: "component", type: NotifierNotificationComponent, selector: "notifier-notification", inputs: ["notification"], outputs: ["ready