UNPKG

@ngxpert/hot-toast

Version:

Smoking hot Notifications for Angular. Lightweight, customizable and beautiful by default.

697 lines (685 loc) 90 kB
import { DOCUMENT, isPlatformServer } from '@angular/common'; import * as i0 from '@angular/core'; import { input, ChangeDetectionStrategy, Component, output, signal, inject, Injector, Renderer2, NgZone, ChangeDetectorRef, ViewChild, Input, ElementRef, afterNextRender, isDevMode, ViewChildren, InjectionToken, PLATFORM_ID, Injectable, makeEnvironmentProviders } from '@angular/core'; import { DynamicViewDirective, isTemplateRef, isComponent, ViewService } from '@ngneat/overview'; import { Subject, race, defer, throwError } from 'rxjs'; import { map, filter, tap, catchError } from 'rxjs/operators'; import { HttpErrorResponse } from '@angular/common/http'; const HOT_TOAST_DEFAULT_TIMEOUTS = { blank: 4000, error: 4000, success: 4000, loading: 30000, warning: 4000, info: 4000, }; const EXIT_ANIMATION_DURATION = 800; const ENTER_ANIMATION_DURATION = 350; const HOT_TOAST_MARGIN = 8; const HOT_TOAST_DEPTH_SCALE = 0.05; const HOT_TOAST_DEPTH_SCALE_ADD = 1; class HotToastRef { constructor(toast) { this.toast = toast; this.groupRefs = []; this.groupExpanded = false; /** Subject for notifying the user that the toast has been closed. */ this._onClosed = new Subject(); /** Subject for notifying the user that the toast has been closed. */ this._onGroupToggle = new Subject(); } set data(data) { this.toast.data = data; } get data() { return this.toast.data; } set dispose(value) { this._dispose = value; } getToast() { return this.toast; } /** * Used for internal purpose * Attach ToastRef to container */ appendTo(container, skipAttachToParent) { const { dispose, updateMessage, updateToast, afterClosed, afterGroupToggled, afterGroupRefsAttached } = container.addToast(this, skipAttachToParent); this.dispose = dispose; this.updateMessage = updateMessage; this.updateToast = updateToast; this.afterClosed = race(this._onClosed.asObservable(), afterClosed); this.afterGroupToggled = race(this._onGroupToggle.asObservable(), afterGroupToggled); this.afterGroupRefsAttached = afterGroupRefsAttached; return this; } /** * Closes the toast * * @param [closeData={ dismissedByAction: false }] - * Make sure to pass { dismissedByAction: true } when closing from template * @memberof HotToastRef */ close(closeData = { dismissedByAction: false }) { this.groupRefs.forEach((ref) => ref.close()); this._dispose(); this._onClosed.next({ dismissedByAction: closeData.dismissedByAction, id: this.toast.id }); this._onClosed.complete(); } toggleGroup(eventData = { byAction: false }) { this.groupExpanded = !this.groupExpanded; this._onGroupToggle.next({ byAction: eventData.byAction, id: this.toast.id, event: this.groupExpanded ? 'expand' : 'collapse', }); } show() { this.toast.visible = true; } } const animate = (renderer, element, animation) => { renderer.setStyle(element, 'animation', animation); }; class LoaderComponent { constructor() { this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : /* istanbul ignore next */ [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: LoaderComponent, isStandalone: true, selector: "hot-toast-loader", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"hot-toast-loader-icon\"\n [style.border-color]=\"theme()?.primary\"\n [style.border-right-color]=\"theme()?.secondary\"\n></div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LoaderComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-loader', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"hot-toast-loader-icon\"\n [style.border-color]=\"theme()?.primary\"\n [style.border-right-color]=\"theme()?.secondary\"\n></div>\n" }] }], propDecorators: { theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }] } }); class ErrorComponent { constructor() { this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : /* istanbul ignore next */ [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ErrorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: ErrorComponent, isStandalone: true, selector: "hot-toast-error", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"hot-toast-error-icon\"\n [style.--error-primary]=\"theme()?.primary\"\n [style.--error-secondary]=\"theme()?.secondary\"\n></div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ErrorComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-error', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "<div\n class=\"hot-toast-error-icon\"\n [style.--error-primary]=\"theme()?.primary\"\n [style.--error-secondary]=\"theme()?.secondary\"\n></div>\n" }] }], propDecorators: { theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }] } }); class CheckMarkComponent { constructor() { this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : /* istanbul ignore next */ [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CheckMarkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: CheckMarkComponent, isStandalone: true, selector: "hot-toast-checkmark", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"hot-toast-checkmark-icon\"\n [style.--check-primary]=\"theme()?.primary\"\n [style.--check-secondary]=\"theme()?.secondary\"\n></div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CheckMarkComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-checkmark', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "<div\n class=\"hot-toast-checkmark-icon\"\n [style.--check-primary]=\"theme()?.primary\"\n [style.--check-secondary]=\"theme()?.secondary\"\n></div>\n" }] }], propDecorators: { theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }] } }); class WarningComponent { constructor() { this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : /* istanbul ignore next */ [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: WarningComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: WarningComponent, isStandalone: true, selector: "hot-toast-warning", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"hot-toast-warning-icon\"\n [style.--warn-primary]=\"theme()?.primary\"\n [style.--warn-secondary]=\"theme()?.secondary\"\n></div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: WarningComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-warning', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "<div\n class=\"hot-toast-warning-icon\"\n [style.--warn-primary]=\"theme()?.primary\"\n [style.--warn-secondary]=\"theme()?.secondary\"\n></div>\n" }] }], propDecorators: { theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }] } }); class InfoComponent { constructor() { this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : /* istanbul ignore next */ [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: InfoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: InfoComponent, isStandalone: true, selector: "hot-toast-info", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n class=\"hot-toast-info-icon\"\n [style.--info-primary]=\"theme()?.primary\"\n [style.--info-secondary]=\"theme()?.secondary\"\n></div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: InfoComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-info', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "<div\n class=\"hot-toast-info-icon\"\n [style.--info-primary]=\"theme()?.primary\"\n [style.--info-secondary]=\"theme()?.secondary\"\n></div>\n" }] }], propDecorators: { theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }] } }); class IndicatorComponent { constructor() { this.theme = input(...(ngDevMode ? [undefined, { debugName: "theme" }] : /* istanbul ignore next */ [])); this.type = input(...(ngDevMode ? [undefined, { debugName: "type" }] : /* istanbul ignore next */ [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: IndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: IndicatorComponent, isStandalone: true, selector: "hot-toast-indicator", inputs: { theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (type() !== 'blank') {\n<div class=\"hot-toast-indicator-wrapper\">\n @if (type() === 'loading') {\n <hot-toast-loader [theme]=\"theme()\" />\n } @if (type() !== 'loading') {\n <div class=\"hot-toast-status-wrapper\">\n <div>\n @switch (type()) { @case ('error') {\n <div>\n <hot-toast-error [theme]=\"theme()\" />\n </div>\n } @case ('success') {\n <div>\n <hot-toast-checkmark [theme]=\"theme()\" />\n </div>\n } @case ('warning') {\n <div>\n <hot-toast-warning [theme]=\"theme()\" />\n </div>\n } @case ('info') {\n <div>\n <hot-toast-info [theme]=\"theme()\" />\n </div>\n } }\n </div>\n </div>\n }\n</div>\n}\n", dependencies: [{ kind: "component", type: LoaderComponent, selector: "hot-toast-loader", inputs: ["theme"] }, { kind: "component", type: ErrorComponent, selector: "hot-toast-error", inputs: ["theme"] }, { kind: "component", type: CheckMarkComponent, selector: "hot-toast-checkmark", inputs: ["theme"] }, { kind: "component", type: WarningComponent, selector: "hot-toast-warning", inputs: ["theme"] }, { kind: "component", type: InfoComponent, selector: "hot-toast-info", inputs: ["theme"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: IndicatorComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-indicator', changeDetection: ChangeDetectionStrategy.OnPush, imports: [LoaderComponent, ErrorComponent, CheckMarkComponent, WarningComponent, InfoComponent], template: "@if (type() !== 'blank') {\n<div class=\"hot-toast-indicator-wrapper\">\n @if (type() === 'loading') {\n <hot-toast-loader [theme]=\"theme()\" />\n } @if (type() !== 'loading') {\n <div class=\"hot-toast-status-wrapper\">\n <div>\n @switch (type()) { @case ('error') {\n <div>\n <hot-toast-error [theme]=\"theme()\" />\n </div>\n } @case ('success') {\n <div>\n <hot-toast-checkmark [theme]=\"theme()\" />\n </div>\n } @case ('warning') {\n <div>\n <hot-toast-warning [theme]=\"theme()\" />\n </div>\n } @case ('info') {\n <div>\n <hot-toast-info [theme]=\"theme()\" />\n </div>\n } }\n </div>\n </div>\n }\n</div>\n}\n" }] }], propDecorators: { theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }] } }); class AnimatedIconComponent { constructor() { this.iconTheme = input(...(ngDevMode ? [undefined, { debugName: "iconTheme" }] : /* istanbul ignore next */ [])); this.icon = input(...(ngDevMode ? [undefined, { debugName: "icon" }] : /* istanbul ignore next */ [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AnimatedIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: AnimatedIconComponent, isStandalone: true, selector: "hot-toast-animated-icon", inputs: { iconTheme: { classPropertyName: "iconTheme", publicName: "iconTheme", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"hot-toast-animated-icon\" [style.color]=\"iconTheme()?.primary\">\n <ng-container *dynamicView=\"icon()\" />\n</div>\n", dependencies: [{ kind: "directive", type: DynamicViewDirective, selector: "[dynamicView]", inputs: ["dynamicView", "dynamicViewInjector", "dynamicViewContext", "dynamicViewInputs"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AnimatedIconComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-animated-icon', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [DynamicViewDirective], template: "<div class=\"hot-toast-animated-icon\" [style.color]=\"iconTheme()?.primary\">\n <ng-container *dynamicView=\"icon()\" />\n</div>\n" }] }], propDecorators: { iconTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTheme", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }] } }); class HotToastGroupItemComponent { constructor() { this.offset = input(0, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ [])); this.defaultConfig = input(...(ngDevMode ? [undefined, { debugName: "defaultConfig" }] : /* istanbul ignore next */ [])); this.toastRef = input(...(ngDevMode ? [undefined, { debugName: "toastRef" }] : /* istanbul ignore next */ [])); this._toastsAfter = 0; this.isShowingAllToasts = input(false, ...(ngDevMode ? [{ debugName: "isShowingAllToasts" }] : /* istanbul ignore next */ [])); this.height = output(); this.beforeClosed = output(); this.afterClosed = output(); this.showAllToasts = output(); this.toggleGroup = output(); this.isManualClose = false; this.toastBarBaseStylesSignal = signal({}, ...(ngDevMode ? [{ debugName: "toastBarBaseStylesSignal" }] : /* istanbul ignore next */ [])); this.unlisteners = []; this.softClosed = false; this.injector = inject(Injector); this.renderer = inject(Renderer2); this.ngZone = inject(NgZone); this.cdr = inject(ChangeDetectorRef); } set toast(value) { this._toast = value; const ogStyle = this.toastBarBaseStylesSignal(); const newStyle = { ...value.style }; if (ogStyle['animation']?.includes('hotToastExitAnimation')) { // if toast is set for exit, we don't need want set the enter animation newStyle['animation'] = ogStyle['animation']; } else { const top = value.position.includes('top'); const enterAnimation = `hotToastEnterAnimation${top ? 'Negative' : 'Positive'} ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; newStyle['animation'] = enterAnimation; } this.toastBarBaseStylesSignal.set(newStyle); } get toast() { return this._toast; } get toastsAfter() { return this._toastsAfter; } set toastsAfter(value) { this._toastsAfter = value; } get toastBarBaseHeight() { return this.toastBarBase.nativeElement.offsetHeight; } get scale() { return this.defaultConfig().stacking !== 'vertical' && !this.isShowingAllToasts() ? this.toastsAfter * -HOT_TOAST_DEPTH_SCALE + 1 : 1; } get translateY() { return this.offset() * (this.top ? 1 : -1) + 'px'; } get exitAnimationDelay() { return this.toast.duration + 'ms'; } get top() { return this.toast.position.includes('top'); } get containerPositionStyle() { const verticalStyle = this.top ? { top: 0 } : { bottom: 0 }; const transform = `translateY(var(--hot-toast-translate-y)) scale(var(--hot-toast-scale))`; const horizontalStyle = this.toast.position.includes('left') ? { left: 0, } : this.toast.position.includes('right') ? { right: 0, } : { left: 0, right: 0, justifyContent: 'center', }; return { transform, ...verticalStyle, ...horizontalStyle, }; } get isIconString() { return typeof this.toast.icon === 'string'; } get groupChildrenToastRefs() { return this.toastRef().groupRefs.filter((ref) => !!ref); } set groupChildrenToastRefs(value) { this.toastRef().groupRefs = value; } get groupChildrenToasts() { return this.groupChildrenToastRefs.map((ref) => ref.getToast()); } get groupHeight() { return this.visibleToasts.map((t) => t.height).reduce((prev, curr) => prev + curr, 0); } get isExpanded() { return this.toastRef().groupExpanded; } ngOnChanges(changes) { if (changes.toast && !changes.toast.firstChange && changes.toast.currentValue?.message) { requestAnimationFrame(() => { this.height.emit(this.toastBarBase.nativeElement.offsetHeight); }); } } ngOnInit() { if (isTemplateRef(this.toast.message)) { this.context = { $implicit: this.toastRef() }; } if (isComponent(this.toast.message)) { this.toastComponentInjector = Injector.create({ providers: [ { provide: HotToastRef, useValue: this.toastRef(), }, ], parent: this.toast.injector || this.injector, }); } const nativeElement = this.toastBarBase.nativeElement; // Caretaker note: `animationstart` and `animationend` events are event tasks that trigger change detection. // We'd want to trigger the change detection only if it's an exit animation. this.ngZone.runOutsideAngular(() => { this.unlisteners.push( // Caretaker note: we have to remove these event listeners at the end (even if the element is removed from DOM). this.renderer.listen(nativeElement, 'animationstart', (event) => { if (this.isExitAnimation(event)) { this.ngZone.run(() => { this.renderer.setStyle(nativeElement, 'pointer-events', 'none'); this.renderer.setStyle(nativeElement.parentElement, 'pointer-events', 'none'); this.beforeClosed.emit(); }); } }), this.renderer.listen(nativeElement, 'animationend', (event) => { if (this.isEnterAnimation(event)) { this.ngZone.run(() => { if (this.toast.autoClose) { const exitAnimation = `hotToastExitAnimation${this.top ? 'Negative' : 'Positive'} ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`; this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); } }); } if (this.isExitAnimation(event)) { this.ngZone.run(() => this.afterClosed.emit({ dismissedByAction: this.isManualClose, id: this.toast.id })); } })); }); } ngAfterViewInit() { const nativeElement = this.toastBarBase.nativeElement; // Caretaker note: accessing `offsetHeight` triggers the whole layout update. // Macro tasks (like `setTimeout`) might be executed within the current rendering frame and cause a frame drop. requestAnimationFrame(() => { this.height.emit(nativeElement.offsetHeight); }); this.setToastAttributes(); } softClose() { const exitAnimation = `hotToastExitSoftAnimation${this.top ? 'Negative' : 'Positive'} ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`; const nativeElement = this.toastBarBase.nativeElement; animate(this.renderer, nativeElement, exitAnimation); this.softClosed = true; } softOpen() { const softEnterAnimation = `hotToastEnterSoftAnimation${top ? 'Negative' : 'Positive'} ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; const nativeElement = this.toastBarBase.nativeElement; animate(this.renderer, nativeElement, softEnterAnimation); this.softClosed = false; } close() { this.isManualClose = true; this.cdr.markForCheck(); const exitAnimation = `hotToastExitAnimation${this.top ? 'Negative' : 'Positive'} ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`; this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); } handleMouseEnter() { this.showAllToasts.emit(true); } handleMouseLeave() { this.showAllToasts.emit(false); } ngOnDestroy() { this.close(); while (this.unlisteners.length) { this.unlisteners.pop()(); } } isExitAnimation(ev) { return ev.animationName.includes('hotToastExitAnimation'); } isEnterAnimation(ev) { return ev.animationName.includes('hotToastEnterAnimation'); } setToastAttributes() { const toastAttributes = this.toast.attributes; for (const [key, value] of Object.entries(toastAttributes)) { this.renderer.setAttribute(this.toastBarBase.nativeElement, key, value); } } get visibleToasts() { return this.groupChildrenToasts.filter((t) => t.visible); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HotToastGroupItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: HotToastGroupItemComponent, isStandalone: true, selector: "hot-toast-group-item", inputs: { toast: { classPropertyName: "toast", publicName: "toast", isSignal: false, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, defaultConfig: { classPropertyName: "defaultConfig", publicName: "defaultConfig", isSignal: true, isRequired: false, transformFunction: null }, toastRef: { classPropertyName: "toastRef", publicName: "toastRef", isSignal: true, isRequired: false, transformFunction: null }, toastsAfter: { classPropertyName: "toastsAfter", publicName: "toastsAfter", isSignal: false, isRequired: false, transformFunction: null }, isShowingAllToasts: { classPropertyName: "isShowingAllToasts", publicName: "isShowingAllToasts", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { height: "height", beforeClosed: "beforeClosed", afterClosed: "afterClosed", showAllToasts: "showAllToasts", toggleGroup: "toggleGroup" }, viewQueries: [{ propertyName: "toastBarBase", first: true, predicate: ["hotToastBarBase"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"hot-toast-bar-base-container\"\n [style]=\"containerPositionStyle\"\n [class]=\"'hot-toast-theme-' + toast.theme\"\n [style.--hot-toast-scale]=\"scale\"\n [style.--hot-toast-translate-y]=\"translateY\"\n>\n <div class=\"hot-toast-bar-base-wrapper\" (mouseenter)=\"handleMouseEnter()\" (mouseleave)=\"handleMouseLeave()\">\n <div\n class=\"hot-toast-bar-base\"\n #hotToastBarBase\n [style]=\"toastBarBaseStylesSignal()\"\n [class]=\"toast.className\"\n [style.--hot-toast-animation-state]=\"isManualClose ? 'running' : 'paused'\"\n [style.--hot-toast-exit-animation-state]=\"isShowingAllToasts() ? 'paused' : 'running'\"\n [style.--hot-toast-exit-animation-delay]=\"exitAnimationDelay\"\n [attr.aria-live]=\"toast.ariaLive\"\n [attr.role]=\"toast.role\"\n >\n <div class=\"hot-toast-icon\" aria-hidden=\"true\">\n @if (toast.icon !== undefined) { @if (isIconString) {\n <hot-toast-animated-icon [iconTheme]=\"toast.iconTheme\">{{ toast.icon }}</hot-toast-animated-icon>\n } @else {\n <div>\n <ng-container *dynamicView=\"toast.icon\" />\n </div>\n } } @else {\n <hot-toast-indicator [theme]=\"toast.iconTheme\" [type]=\"toast.type\" />\n }\n </div>\n <div class=\"hot-toast-message\">\n <ng-container *dynamicView=\"toast.message; context: context; injector: toastComponentInjector\" />\n </div>\n @if (toast.dismissible) {\n <button\n (click)=\"close()\"\n type=\"button\"\n class=\"hot-toast-close-btn\"\n aria-label=\"Close\"\n [style]=\"toast.closeStyle\"\n ></button>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: AnimatedIconComponent, selector: "hot-toast-animated-icon", inputs: ["iconTheme", "icon"] }, { kind: "component", type: IndicatorComponent, selector: "hot-toast-indicator", inputs: ["theme", "type"] }, { kind: "directive", type: DynamicViewDirective, selector: "[dynamicView]", inputs: ["dynamicView", "dynamicViewInjector", "dynamicViewContext", "dynamicViewInputs"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HotToastGroupItemComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-group-item', changeDetection: ChangeDetectionStrategy.OnPush, imports: [AnimatedIconComponent, IndicatorComponent, DynamicViewDirective], template: "<div\n class=\"hot-toast-bar-base-container\"\n [style]=\"containerPositionStyle\"\n [class]=\"'hot-toast-theme-' + toast.theme\"\n [style.--hot-toast-scale]=\"scale\"\n [style.--hot-toast-translate-y]=\"translateY\"\n>\n <div class=\"hot-toast-bar-base-wrapper\" (mouseenter)=\"handleMouseEnter()\" (mouseleave)=\"handleMouseLeave()\">\n <div\n class=\"hot-toast-bar-base\"\n #hotToastBarBase\n [style]=\"toastBarBaseStylesSignal()\"\n [class]=\"toast.className\"\n [style.--hot-toast-animation-state]=\"isManualClose ? 'running' : 'paused'\"\n [style.--hot-toast-exit-animation-state]=\"isShowingAllToasts() ? 'paused' : 'running'\"\n [style.--hot-toast-exit-animation-delay]=\"exitAnimationDelay\"\n [attr.aria-live]=\"toast.ariaLive\"\n [attr.role]=\"toast.role\"\n >\n <div class=\"hot-toast-icon\" aria-hidden=\"true\">\n @if (toast.icon !== undefined) { @if (isIconString) {\n <hot-toast-animated-icon [iconTheme]=\"toast.iconTheme\">{{ toast.icon }}</hot-toast-animated-icon>\n } @else {\n <div>\n <ng-container *dynamicView=\"toast.icon\" />\n </div>\n } } @else {\n <hot-toast-indicator [theme]=\"toast.iconTheme\" [type]=\"toast.type\" />\n }\n </div>\n <div class=\"hot-toast-message\">\n <ng-container *dynamicView=\"toast.message; context: context; injector: toastComponentInjector\" />\n </div>\n @if (toast.dismissible) {\n <button\n (click)=\"close()\"\n type=\"button\"\n class=\"hot-toast-close-btn\"\n aria-label=\"Close\"\n [style]=\"toast.closeStyle\"\n ></button>\n }\n </div>\n </div>\n</div>\n" }] }], propDecorators: { toast: [{ type: Input }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], defaultConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultConfig", required: false }] }], toastRef: [{ type: i0.Input, args: [{ isSignal: true, alias: "toastRef", required: false }] }], toastsAfter: [{ type: Input }], isShowingAllToasts: [{ type: i0.Input, args: [{ isSignal: true, alias: "isShowingAllToasts", required: false }] }], height: [{ type: i0.Output, args: ["height"] }], beforeClosed: [{ type: i0.Output, args: ["beforeClosed"] }], afterClosed: [{ type: i0.Output, args: ["afterClosed"] }], showAllToasts: [{ type: i0.Output, args: ["showAllToasts"] }], toggleGroup: [{ type: i0.Output, args: ["toggleGroup"] }], toastBarBase: [{ type: ViewChild, args: ['hotToastBarBase', { static: true }] }] } }); class HotToastComponent { constructor() { this.offset = input(0, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ [])); this.defaultConfig = input(...(ngDevMode ? [undefined, { debugName: "defaultConfig" }] : /* istanbul ignore next */ [])); this.toastRef = input(...(ngDevMode ? [undefined, { debugName: "toastRef" }] : /* istanbul ignore next */ [])); this._toastsAfter = 0; this.isShowingAllToasts = input(false, ...(ngDevMode ? [{ debugName: "isShowingAllToasts" }] : /* istanbul ignore next */ [])); this.height = output(); this.beforeClosed = output(); this.afterClosed = output(); this.showAllToasts = output(); this.toggleGroup = output(); this.isManualClose = false; this.isExpanded = false; this.toastBarBaseStylesSignal = signal({}, ...(ngDevMode ? [{ debugName: "toastBarBaseStylesSignal" }] : /* istanbul ignore next */ [])); this.unlisteners = []; this.softClosed = false; this.groupRefs = []; this.injector = inject(Injector); this.renderer = inject(Renderer2); this.ngZone = inject(NgZone); this.cdr = inject(ChangeDetectorRef); } set toast(value) { this._toast = value; const ogStyle = this.toastBarBaseStylesSignal(); const newStyle = { ...value.style }; if (ogStyle['animation']?.includes('hotToastExitAnimation')) { // if toast is set for exit, we don't need want set the enter animation newStyle['animation'] = ogStyle['animation']; } else { const top = value.position.includes('top'); const enterAnimation = `hotToastEnterAnimation${top ? 'Negative' : 'Positive'} ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; newStyle['animation'] = enterAnimation; } this.toastBarBaseStylesSignal.set(newStyle); } get toast() { return this._toast; } get toastsAfter() { return this._toastsAfter; } set toastsAfter(value) { this._toastsAfter = value; const defaultConfig = this.defaultConfig(); if (defaultConfig?.visibleToasts > 0) { if (this.toast.autoClose) { // if (value >= this.defaultConfig?.visibleToasts) { // this.close(); // } } else { if (value >= defaultConfig?.visibleToasts) { this.softClose(); } else if (this.softClosed) { this.softOpen(); } } } } get toastBarBaseHeight() { return this.toastBarBase.nativeElement.offsetHeight; } get scale() { return this.defaultConfig().stacking !== 'vertical' && !this.isShowingAllToasts() ? this.toastsAfter * -HOT_TOAST_DEPTH_SCALE + 1 : 1; } get translateY() { return this.offset() * (this.top ? 1 : -1) + 'px'; } get exitAnimationDelay() { return this.toast.duration + 'ms'; } get top() { return this.toast.position.includes('top'); } get containerPositionStyle() { const verticalStyle = this.top ? { top: 0 } : { bottom: 0 }; const transform = `translateY(var(--hot-toast-translate-y)) scale(var(--hot-toast-scale))`; const horizontalStyle = this.toast.position.includes('left') ? { left: 0, } : this.toast.position.includes('right') ? { right: 0, } : { left: 0, right: 0, justifyContent: 'center', }; return { transform, ...verticalStyle, ...horizontalStyle, }; } get isIconString() { return typeof this.toast.icon === 'string'; } get groupChildrenToastRefs() { return this.groupRefs.filter((ref) => !!ref); } set groupChildrenToastRefs(value) { this.groupRefs = value; this.toastRef().groupRefs = value; } get groupChildrenToasts() { return this.groupChildrenToastRefs.map((ref) => ref.getToast()); } get groupHeight() { return this.visibleToasts .slice(-this.defaultConfig().visibleToasts) .map((t) => t.height) .reduce((prev, curr) => prev + curr, 0); } get visibleToasts() { return this.groupChildrenToasts.filter((t) => t.visible); } ngDoCheck() { const toastRef = this.toastRef(); if (toastRef.groupRefs.length !== this.groupRefs.length) { this.groupRefs = toastRef.groupRefs.slice(); this.cdr.markForCheck(); this.emiHeightWithGroup(this.isExpanded); } if (toastRef.groupExpanded !== this.isExpanded) { this.isExpanded = toastRef.groupExpanded; this.cdr.markForCheck(); this.emiHeightWithGroup(this.isExpanded); } } ngOnChanges(changes) { if (changes.toast && !changes.toast.firstChange && changes.toast.currentValue?.message) { (this, this.emiHeightWithGroup(this.isExpanded)); } } ngOnInit() { if (isTemplateRef(this.toast.message)) { this.context = { $implicit: this.toastRef() }; } if (isComponent(this.toast.message)) { this.toastComponentInjector = Injector.create({ providers: [ { provide: HotToastRef, useValue: this.toastRef(), }, ], parent: this.toast.injector || this.injector, }); } const nativeElement = this.toastBarBase.nativeElement; // Caretaker note: `animationstart` and `animationend` events are event tasks that trigger change detection. // We'd want to trigger the change detection only if it's an exit animation. this.ngZone.runOutsideAngular(() => { this.unlisteners.push( // Caretaker note: we have to remove these event listeners at the end (even if the element is removed from DOM). this.renderer.listen(nativeElement, 'animationstart', (event) => { if (this.isExitAnimation(event)) { this.ngZone.run(() => { this.renderer.setStyle(nativeElement, 'pointer-events', 'none'); this.renderer.setStyle(nativeElement.parentElement, 'pointer-events', 'none'); this.beforeClosed.emit(); }); } }), this.renderer.listen(nativeElement, 'animationend', (event) => { if (this.isEnterAnimation(event)) { this.ngZone.run(() => { if (this.toast.autoClose) { const exitAnimation = `hotToastExitAnimation${this.top ? 'Negative' : 'Positive'} ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`; this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); } }); } if (this.isExitAnimation(event)) { this.ngZone.run(() => this.afterClosed.emit({ dismissedByAction: this.isManualClose, id: this.toast.id })); } })); }); } ngAfterViewInit() { const nativeElement = this.toastBarBase.nativeElement; // Caretaker note: accessing `offsetHeight` triggers the whole layout update. // Macro tasks (like `setTimeout`) might be executed within the current rendering frame and cause a frame drop. requestAnimationFrame(() => { this.height.emit(nativeElement.offsetHeight); }); this.setToastAttributes(); } softClose() { const exitAnimation = `hotToastExitSoftAnimation${this.top ? 'Negative' : 'Positive'} ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`; const nativeElement = this.toastBarBase.nativeElement; animate(this.renderer, nativeElement, exitAnimation); this.softClosed = true; if (this.isExpanded) { this.toggleToastGroup(); } } softOpen() { const softEnterAnimation = `hotToastEnterSoftAnimation${top ? 'Negative' : 'Positive'} ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; const nativeElement = this.toastBarBase.nativeElement; animate(this.renderer, nativeElement, softEnterAnimation); this.softClosed = false; } close() { this.isManualClose = true; this.cdr.markForCheck(); const exitAnimation = `hotToastExitAnimation${this.top ? 'Negative' : 'Positive'} ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`; this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); } handleMouseEnter() { this.showAllToasts.emit(true); } handleMouseLeave() { this.showAllToasts.emit(false); } ngOnDestroy() { this.close(); while (this.unlisteners.length) { this.unlisteners.pop()(); } } isExitAnimation(ev) { return ev.animationName.includes('hotToastExitAnimation'); } isEnterAnimation(ev) { return ev.animationName.includes('hotToastEnterAnimation'); } setToastAttributes() { const toastAttributes = this.toast.attributes; for (const [key, value] of Object.entries(toastAttributes)) { this.renderer.setAttribute(this.toastBarBase.nativeElement, key, value); } } calculateOffset(toastId) { const visibleToasts = this.visibleToasts; const index = visibleToasts.findIndex((toast) => toast.id === toastId); const offset = index !== -1 ? visibleToasts.slice(...(this.defaultConfig().reverseOrder ? [index + 1] : [0, index])).reduce((acc, t, i) => { const defaultConfig = this.defaultConfig(); return defaultConfig.visibleToasts !== 0 && i < visibleToasts.length - defaultConfig.visibleToasts ? 0 : acc + (t.height || 0); }, 0) : 0; return offset; } updateHeight(height, toast) { toast.height = height; this.cdr.markForCheck(); } beforeClosedGroupItem(toast) { toast.visible = false; this.cdr.markForCheck(); if (this.visibleToasts.length === 0 && this.isExpanded) { this.toggleToastGroup(); } else { this.emiHeightWithGroup(this.isExpanded); } } afterClosedGroupItem(closeToast) { const toastIndex = this.groupChildrenToasts.findIndex((t) => t.id === closeToast.id); if (toastIndex > -1) { this.groupChildrenToastRefs = this.groupChildrenToastRefs.filter((t) => t.getToast().id !== closeToast.id); this.cdr.markForCheck(); } } toggleToastGroup() { const event = this.isExpanded ? 'collapse' : 'expand'; this.toggleGroup.emit({ byAction: true, event, id: this.toast.id, }); this.emiHeightWithGroup(event === 'expand'); } emiHeightWithGroup(isExpanded) { if (isExpanded) { requestAnimationFrame(() => { this.height.emit(this.toastBarBase.nativeElement.offsetHeight + this.groupHeight); }); } else { requestAnimationFrame(() => { this.height.emit(this.toastBarBase.nativeElement.offsetHeight); }); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HotToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: HotToastComponent, isStandalone: true, selector: "hot-toast-component", inputs: { toast: { classPropertyName: "toast", publicName: "toast", isSignal: false, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, defaultConfig: { classPropertyName: "defaultConfig", publicName: "defaultConfig", isSignal: true, isRequired: false, transformFunction: null }, toastRef: { classPropertyName: "toastRef", publicName: "toastRef", isSignal: true, isRequired: false, transformFunction: null }, toastsAfter: { classPropertyName: "toastsAfter", publicName: "toastsAfter", isSignal: false, isRequired: false, transformFunction: null }, isShowingAllToasts: { classPropertyName: "isShowingAllToasts", publicName: "isShowingAllToasts", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { height: "height", beforeClosed: "beforeClosed", afterClosed: "afterClosed", showAllToasts: "showAllToasts", toggleGroup: "toggleGroup" }, viewQueries: [{ propertyName: "toastBarBase", first: true, predicate: ["hotToastBarBase"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"hot-toast-bar-base-container\"\n [style]=\"containerPositionStyle\"\n [class]=\"'hot-toast-theme-' + toast.theme\"\n [style.--hot-toast-scale]=\"scale\"\n [style.--hot-toast-translate-y]=\"translateY\"\n>\n <div\n class=\"hot-toast-bar-base-wrapper\"\n [class.expanded]=\"isExpanded\"\n (mouseenter)=\"handleMouseEnter()\"\n (mouseleave)=\"handleMouseLeave()\"\n >\n <div\n class=\"hot-toast-bar-base\"\n #hotToastBarBase\n [style]=\"toastBarBaseStylesSignal()\"\n [class]=\"toast.className\"\n [style.--hot-toast-animation-state]=\"isManualClose ? 'running' : 'paused'\"\n [style.--hot-toast-exit-animation-state]=\"isShowingAllToasts() ? 'paused' : 'running'\"\n [style.--hot-toast-exit-animation-delay]=\"exitAnimationDelay\"\n [attr.aria-live]=\"toast.ariaLive\"\n [attr.role]=\"toast.role\"\n >\n <div class=\"hot-toast-icon\" aria-hidden=\"true\">\n @if (toast.icon !== undefined) { @if (isIconString) {\n <hot-toast-animated-icon [iconTheme]=\"toast.iconTheme\" [icon]=\"toast.icon\" />\n } @else {\n <div>\n <ng-container *dynamicView=\"toast.icon\" />\n </div>\n } } @else {\n <hot-toast-indicator [theme]=\"toast.iconTheme\" [type]=\"toast.type\" />\n }\n </div>\n\n <div class=\"hot-toast-message\">\n <ng-container *dynamicView=\"toast.message; context: context; injector: toastComponentInjector\" />\n </div>\n\n @if (toast.group?.expandAndCollapsible && toast.group?.children && visibleToasts.length > 0) {\n <button\n (click)=\"toggleToastGroup()\"\n type=\"button\"\n class=\"hot-toast-group-btn\"\n [class.expanded]=\"isExpanded\"\n [attr.aria-label]=\"isExpanded ? 'Collapse' : 'Expand'\"\n [style]=\"toast.group.btnStyle\"\n ></button>\n } @if (toast.dismissible) {\n <button\n (click)=\"close()\"\n type=\"button\"\n class=\"hot-toast-close-btn\"\n aria-label=\"Close\"\n [style]=\"toast.closeStyle\"\n ></button>\n }\n </div>\n\n @if (toast.visible) {\n <div\n role=\"list\"\n class=\"hot-toast-bar-base-group\"\n [class]=\"toast.group?.className\"\n [style.--hot-toast-group-height]=\"groupHeight + 'px'\"\n >\n @for (item of groupChildrenToasts; track item.id) {\n <hot-toast-group-item [toast]=\"item\"\n [offset]=\"calculateOffset(item.id)\"\n [toastRef]=\"toastRef().groupRefs[$index]\"\n [toastsAfter]=\"(item.autoClose ? groupChildrenToasts.length : visibleToasts.length) - 1 - $index\"\n [defaultConfig]=\"defaultConfig()\"\n [isShowingAllToasts]=\"isShowingAllToasts()\"\n (height)=\"updateHeight($event, item)\"\n (beforeClosed)=\"beforeClosedGroupItem(item)\"\n (afterClosed)=\"afterClosedGroupItem($event)\"\n />\n }\n </div>\n }\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: DynamicViewDirective, selector: "[dynamicView]", inputs: ["dynamicView", "dynamicViewInjector", "dynamicViewContext", "dynamicViewInputs"] }, { kind: "component", type: IndicatorComponent, selector: "hot-toast-indicator", inputs: ["theme", "type"] }, { kind: "component", type: AnimatedIconComponent, selector: "hot-toast-animated-icon", inputs: ["iconTheme", "icon"] }, { kind: "component", type: HotToastGroupItemComponent, selector: "hot-toast-group-item", inputs: ["toast", "offset", "defaultConfig", "toastRef", "toastsAfter", "isShowingAllToasts"], outputs: ["height", "beforeClosed", "afterClosed", "showAllToasts", "toggleGroup"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HotToastComponent, decorators: [{ type: Component, args: [{ selector: 'hot-toast-component', changeDetection: ChangeDetectionStrategy.OnPush, imports: [DynamicViewDirective, IndicatorComponent, AnimatedIconComponent, HotToastGroupItemComponent], template: "<div\n class=\"hot-toast-bar-base-container\"\n [style]=\"containerPositionStyle\"\n [class]=\"'hot-toast-theme-' + toast.theme\"\n [style.--hot-toast-scale]=\"scale\"\n [style.--hot-toast-translate-y]=\"translateY\"\n>\n <div\n class=\"hot-toast-bar-base-wrapper\"\n [class.expanded]=\"isExpanded\"\n (mouseenter)=\"handleMouseEnter()\"\n (mouseleave)=\"handleMouseLeave()\"\n >\n <div\n class=\"hot-toast-bar-base\"\n #hotToastBarBase\n [style]=\"toastBarBaseStylesSignal()\"\n [class]=\"toast.cl