@ngxpert/hot-toast
Version:
Smoking hot Notifications for Angular. Lightweight, customizable and beautiful by default.
1,417 lines • 77.2 kB
JavaScript
import { NgStyle, isPlatformServer } from '@angular/common';
import * as i0 from '@angular/core';
import { Input, ChangeDetectionStrategy, Component, EventEmitter, signal, inject, Injector, Renderer2, NgZone, ChangeDetectorRef, ViewChild, Output, ViewChildren, PLATFORM_ID, Injectable, makeEnvironmentProviders } from '@angular/core';
import { DynamicViewDirective, isTemplateRef, isComponent, ViewService } from '@ngneat/overview';
import { Subject, race, defer } from 'rxjs';
import { map, filter, tap } from 'rxjs/operators';
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 {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: LoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", type: LoaderComponent, isStandalone: true, selector: "hot-toast-loader", inputs: { theme: "theme" }, 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: "20.0.0", 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: Input
}] } });
class ErrorComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ErrorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", type: ErrorComponent, isStandalone: true, selector: "hot-toast-error", inputs: { theme: "theme" }, 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: "20.0.0", 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: Input
}] } });
class CheckMarkComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: CheckMarkComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", type: CheckMarkComponent, isStandalone: true, selector: "hot-toast-checkmark", inputs: { theme: "theme" }, 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: "20.0.0", 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: Input
}] } });
class WarningComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: WarningComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", type: WarningComponent, isStandalone: true, selector: "hot-toast-warning", inputs: { theme: "theme" }, 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: "20.0.0", 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: Input
}] } });
class InfoComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: InfoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", type: InfoComponent, isStandalone: true, selector: "hot-toast-info", inputs: { theme: "theme" }, 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: "20.0.0", 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: Input
}] } });
class IndicatorComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: IndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: IndicatorComponent, isStandalone: true, selector: "hot-toast-indicator", inputs: { theme: "theme", type: "type" }, ngImport: i0, template: "@if (type !== 'blank') {\n<div class=\"hot-toast-indicator-wrapper\">\n @if (type === 'loading') {\n <hot-toast-loader [theme]=\"theme\"></hot-toast-loader>\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\"></hot-toast-error>\n </div>\n } @case ('success') {\n <div>\n <hot-toast-checkmark [theme]=\"theme\"></hot-toast-checkmark>\n </div>\n } @case ('warning') {\n <div>\n <hot-toast-warning [theme]=\"theme\"></hot-toast-warning>\n </div>\n } @case ('info') {\n <div>\n <hot-toast-info [theme]=\"theme\"></hot-toast-info>\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: "20.0.0", 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\"></hot-toast-loader>\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\"></hot-toast-error>\n </div>\n } @case ('success') {\n <div>\n <hot-toast-checkmark [theme]=\"theme\"></hot-toast-checkmark>\n </div>\n } @case ('warning') {\n <div>\n <hot-toast-warning [theme]=\"theme\"></hot-toast-warning>\n </div>\n } @case ('info') {\n <div>\n <hot-toast-info [theme]=\"theme\"></hot-toast-info>\n </div>\n } }\n </div>\n </div>\n }\n</div>\n}\n" }]
}], propDecorators: { theme: [{
type: Input
}], type: [{
type: Input
}] } });
class AnimatedIconComponent {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: AnimatedIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", type: AnimatedIconComponent, isStandalone: true, selector: "hot-toast-animated-icon", inputs: { iconTheme: "iconTheme", icon: "icon" }, ngImport: i0, template: "<div class=\"hot-toast-animated-icon\" [style.color]=\"iconTheme?.primary\">\n <ng-container *dynamicView=\"icon\"></ng-container>\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: "20.0.0", 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\"></ng-container>\n</div>\n" }]
}], propDecorators: { iconTheme: [{
type: Input
}], icon: [{
type: Input
}] } });
class HotToastGroupItemComponent {
constructor() {
this.offset = 0;
this._toastsAfter = 0;
this.isShowingAllToasts = false;
this.height = new EventEmitter();
this.beforeClosed = new EventEmitter();
this.afterClosed = new EventEmitter();
this.showAllToasts = new EventEmitter();
this.toggleGroup = new EventEmitter();
this.isManualClose = false;
this.toastBarBaseStylesSignal = signal({});
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).
// zone.js stores its `ZoneTask`s within the `nativeElement[Zone.__symbol__('animationstart') + 'false']` property
// with callback that capture `this`.
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: "20.0.0", ngImport: i0, type: HotToastGroupItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: HotToastGroupItemComponent, isStandalone: true, selector: "hot-toast-group-item", inputs: { toast: "toast", offset: "offset", defaultConfig: "defaultConfig", toastRef: "toastRef", toastsAfter: "toastsAfter", isShowingAllToasts: "isShowingAllToasts" }, 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\"></ng-container>\n </div>\n } } @else {\n <hot-toast-indicator [theme]=\"toast.iconTheme\" [type]=\"toast.type\"></hot-toast-indicator>\n }\n </div>\n <div class=\"hot-toast-message\">\n <ng-container *dynamicView=\"toast.message; context: context; injector: toastComponentInjector\"></ng-container>\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: "20.0.0", 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\"></ng-container>\n </div>\n } } @else {\n <hot-toast-indicator [theme]=\"toast.iconTheme\" [type]=\"toast.type\"></hot-toast-indicator>\n }\n </div>\n <div class=\"hot-toast-message\">\n <ng-container *dynamicView=\"toast.message; context: context; injector: toastComponentInjector\"></ng-container>\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: Input
}], defaultConfig: [{
type: Input
}], toastRef: [{
type: Input
}], toastsAfter: [{
type: Input
}], isShowingAllToasts: [{
type: Input
}], height: [{
type: Output
}], beforeClosed: [{
type: Output
}], afterClosed: [{
type: Output
}], showAllToasts: [{
type: Output
}], toggleGroup: [{
type: Output
}], toastBarBase: [{
type: ViewChild,
args: ['hotToastBarBase', { static: true }]
}] } });
class HotToastComponent {
constructor() {
this.offset = 0;
this._toastsAfter = 0;
this.isShowingAllToasts = false;
this.height = new EventEmitter();
this.beforeClosed = new EventEmitter();
this.afterClosed = new EventEmitter();
this.showAllToasts = new EventEmitter();
this.toggleGroup = new EventEmitter();
this.isManualClose = false;
this.isExpanded = false;
this.toastBarBaseStylesSignal = signal({});
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;
if (this.defaultConfig?.visibleToasts > 0) {
if (this.toast.autoClose) {
// if (value >= this.defaultConfig?.visibleToasts) {
// this.close();
// }
}
else {
if (value >= this.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() {
if (this.toastRef.groupRefs.length !== this.groupRefs.length) {
this.groupRefs = this.toastRef.groupRefs.slice();
this.cdr.markForCheck();
this.emiHeightWithGroup(this.isExpanded);
}
if (this.toastRef.groupExpanded !== this.isExpanded) {
this.isExpanded = this.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).
// zone.js stores its `ZoneTask`s within the `nativeElement[Zone.__symbol__('animationstart') + 'false']` property
// with callback that capture `this`.
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) => {
return this.defaultConfig.visibleToasts !== 0 && i < visibleToasts.length - this.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: "20.0.0", ngImport: i0, type: HotToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: HotToastComponent, isStandalone: true, selector: "hot-toast", inputs: { toast: "toast", offset: "offset", defaultConfig: "defaultConfig", toastRef: "toastRef", toastsAfter: "toastsAfter", isShowingAllToasts: "isShowingAllToasts" }, 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\"></hot-toast-animated-icon>\n } @else {\n <div>\n <ng-container *dynamicView=\"toast.icon\"></ng-container>\n </div>\n } } @else {\n <hot-toast-indicator [theme]=\"toast.iconTheme\" [type]=\"toast.type\"></hot-toast-indicator>\n }\n </div>\n\n <div class=\"hot-toast-message\">\n <ng-container *dynamicView=\"toast.message; context: context; injector: toastComponentInjector\"></ng-container>\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\n [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 ></hot-toast-group-item>\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: "20.0.0", ngImport: i0, type: HotToastComponent, decorators: [{
type: Component,
args: [{ selector: 'hot-toast', 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.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\"></hot-toast-animated-icon>\n } @else {\n <div>\n <ng-container *dynamicView=\"toast.icon\"></ng-container>\n </div>\n } } @else {\n <hot-toast-indicator [theme]=\"toast.iconTheme\" [type]=\"toast.type\"></hot-toast-indicator>\n }\n </div>\n\n <div class=\"hot-toast-message\">\n <ng-container *dynamicView=\"toast.message; context: context; injector: toastComponentInjector\"></ng-container>\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\n [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 ></hot-toast-group-item>\n }\n </div>\n }\n </div>\n</div>\n" }]
}], propDecorators: { toast: [{
type: Input
}], offset: [{
type: Input
}], defaultConfig: [{
type: Input
}], toastRef: [{
type: Input
}], toastsAfter: [{
type: Input
}], isShowingAllToasts: [{
type: Input
}], height: [{
type: Output
}], beforeClosed: [{
type: Output
}], afterClosed: [{
type: Output
}], showAllToasts: [{
type: Output
}], toggleGroup: [{
type: Output
}], toastBarBase: [{
type: ViewChild,
args: ['hotToastBarBase', { static: true }]
}] } });
class HotToastContainerComponent {
constructor() {
this.toasts = [];
this.toastRefs = [];
this.isShowingAllToasts = 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 expanded or collapsed. */
this._onGroupToggle = new Subject();
/** Subject for notifying the user that the group refs have been attached to toast. */
this._onGroupRefAttached = new Subject();
this.onClosed$ = this._onClosed.asObservable();
this.onGroupToggle$ = this._onGroupToggle.asObservable();
this.onGroupRefAttached$ = this._onGroupRefAttached.asObservable();
this.cdr = inject(ChangeDetectorRef);
this.toastService = inject(HotToastService);
}
trackById(index, toast) {
return toast.id;
}
getVisibleToasts(position) {
return this.unGroupedToasts.filter((t) => t.visible && t.position === position);
}
get unGroupedToasts() {
return this.toasts.filter((t) => t.group?.parent === undefined || t.group?.children === undefined || t.group?.children.length === 0);
}
calculateOffset(toastId, position) {
const visibleToasts = this.getVisibleToasts(position);
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 toastsAfter = visibleToasts.length - 1 - i;
return this.defaultConfig.visibleToasts !== 0 && i < visibleToasts.length - this.defaultConfig.visibleToasts
? 0
: acc +
(this.defaultConfig.stacking === 'vertical' || this.isShowingAllToasts
? t.height || 0
: toastsAfter * HOT_TOAST_DEPTH_SCALE + HOT_TOAST_DEPTH_SCALE_ADD) +
HOT_TOAST_MARGIN;
}, 0)
: 0;
return offset;
}
updateHeight(height, toast) {
toast.height = height;
this.cdr.markForCheck();
}
addToast(ref, skipAttachToParent) {
this.toastRefs.push(ref);
const toast = ref.getToast();
this.toasts.push(ref.getToast());
if (this.defaultConfig.visibleToasts !== 0 && this.unGroupedToasts.length > this.defaultConfig.visibleToasts) {
const closeToasts = this.toasts.slice(0, this.toasts.length - this.defaultConfig.visibleToasts);
closeToasts.forEach((t) => {
if (t.autoClose) {
this.closeToast(t.id);
}
});
}
this.cdr.markForCheck();
this.attachGroupRefs(toast, ref, skipAttachToParent);
return {
dispose: () => {
this.closeToast(toast.id);
},
updateMessage: (message) => {
toast.message = message;
this.updateToasts(toast);
this.cdr.markForCheck();
},
updateToast: (options) => {
this.updateToasts(toast, options);
this.cdr.markForCheck();
},
afterClosed: this.getAfterClosed(toast),
afterGroupToggled: this.getAfterGroupToggled(toast),
afterGroupRefsAttached: this.getAfterGroupRefsAttached(toast).pipe(map((v) => v.groupRefs)),
};
}
async attachGroupRefs(toast, ref, skipAttachToParent) {
let groupRefs = [];
if (toast.group) {
if (toast.group.children) {
groupRefs = await this.createGroupRefs(toast, ref);
const toastIndex = this.toastRefs.findIndex((t) => t.getToast().id === toast.id);
if (toastIndex > -1) {
this.toastRefs[toastIndex].groupRefs = groupRefs;
this.cdr.markForCheck();
this._onGroupRefAttached.next({ groupRefs, id: toast.id });
}
}
else if (toast.group.parent && !skipAttachToParent) {
const parentToastRef = toast.group.parent;
const parentToast = parentToastRef.getToast();
const parentToastRefIndex = this.toastRefs.findIndex((t) => t.getToast().id === parentToast.id);
const parentToastIndex = this.toasts.findIndex((t) => t.id === parentToast.id);
if (parentToastRefIndex > -1 && parentToastIndex > -1) {
this.toastRefs[parentToastRefIndex].groupRefs.push(ref);
const existingGroup = this.toasts[parentToastRefIndex].group ?? {};
const existingChildren = this.toasts[parentToastRefIndex].group?.children ?? [];
existingChildren.push({ options: { ...toast, type: toast.type, message: toast.message } });
existingGroup.children = existingChildren;
this.toasts[parentToastRefIndex].group = { ...existingGroup };
this.cdr.markForCheck();
this._onGroupRefAttached.next({ groupRefs, id: parentToast.id });
}
}
}
}
createGroupRefs(toast, ref) {
const skipAttachToParent = true;
return new Promise((resolve) => {
const items = toast.group.children;
const allPromises = items.map((item) => {
return new Promise((innerResolve) => {
item.options.group = { parent: ref };
// We need to give a tick's delay so that IDs are generated properly
setTimeout(() => {
try {
const itemRef = this.toastService.show(item.options.message, item.options, skipAttachToParent);
innerResolve(itemRef);
}
catch (error) {
console.error('Error creating toast', error);
innerResolve(null);
}
});
});
});
Promise.all(allPromises).then((refs) => resolve(refs));
});
}
closeToast(id) {
if (id) {
const comp = this.hotToastComponentList.find((item) => item.toast.id === id);
if (comp) {
comp.close();
this.cdr.markForCheck();
}
}
else {
this.hotToastComponentList.forEach((comp) => comp.close());
this.cdr.markForCheck();
}
}
beforeClosed(toast) {
toast.visible = false;
this.cdr.markForCheck();
}
afterClosed(closeToast) {
const toastIndex = this.toasts.findIndex((t) => t.id === closeToast.id);
if (toastIndex > -1) {
this._onClosed.next(closeToast);
this.toasts = this.toasts.filter((t) => t.id !== closeToast.id);
this.toastRefs = this.toastRefs.filter((t) => t.getToast().id !== closeToast.id);
this.cdr.markForCheck();
}
}
toggleGroup(groupEvent) {
const toastIndex = this.toastRefs.findIndex((t) => t.getToast().id === groupEvent.id);
if (toastIndex > -1) {
this._onGroupToggle.next(groupEvent);
this.toastRefs[toastIndex].groupExpanded = groupEvent.event === 'expand';
this.cdr.markForCheck();
}
}
hasToast(id) {
return this.toasts.findIndex((t) => t.id === id) > -1;
}
showAllToasts(show) {
this.isShowingAllToasts = show;
}
getAfterClosed(toast) {
return this.onClosed$.pipe(filter((v) => v.id === toast.id));
}
getAfterGroupToggled(toast) {
return this.onGroupToggle$.pipe(filter((v) => v.id === toast.id));
}
getAfterGroupRefsAttached(toast) {
return this.onGroupRefAttached$.pipe(filter((v) => v.id === toast.id));
}
updateToasts(toast, options) {
this.toasts = this.toasts.map((t) => ({ ...t, ...(t.id === toast.id && { ...toast, ...options }) }));
this.cdr.markForCheck();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: HotToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: HotToastContainerComponent, isStandalone: true, selector: "hot-toast-container", inputs: { defaultConfig: "defaultConfig" }, viewQueries: [{ propertyName: "hotToastComponentList", predicate: HotToastComponent, descendants: true }], ngImport: i0, template: "<div [ngStyle]=\"{position: 'fixed', zIndex: 9999, top: 0, right: 0, bottom: 0, left: 0, pointerEvents: 'none'}\">\n <div [ngStyle]=\"{position: 'relative', height: '100%'}\">\n <div>\n @for (toast of toasts; track trackById(i, toast); let i = $index) { @if (toast.group?.parent) {} @else {\n <hot-toast\n [toast]=\"toast\"\n [offset]=\"calculateOffset(toast.id, toast.position)\"\n [toastRef]=\"toastRefs[i]\"\n [toastsAfter]=\"(toast.autoClose ? toasts.length : getVisibleToasts(toast.position).length) - 1 - i\"\n [defaultConfig]=\"defaultConfig\"\n [isShowingAllToasts]=\"isShowingAllToasts\"\n (showAllToasts)=\"showAllToasts($event)\"\n (height)=\"updateHeight($event, toast)\"\n (beforeClosed)=\"beforeClosed(toast)\"\n (afterClosed)=\"afterClosed($event)\"\n (toggleGroup)=\"toggleGroup($event)\"\n ></hot-toast>\n } }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HotToastComponent, selector: "hot-toast", inputs: ["toast", "offset", "defaultConfig", "toastRef", "toastsAfter", "isShowingAllToasts"], outputs: ["height", "beforeClosed", "afterClosed", "showAllToasts", "toggleGroup"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, preserveWhitespaces: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: HotToastContainerComponent, decorators: [{
type: Component,
args: [{ selector: 'hot-toast-container', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HotToastComponent, NgStyle], template: "<div [ngStyle]=\"{position: 'fixed', zIndex: 9999, top: 0, right: 0, bottom: 0, left: 0, pointerEvents: 'none'}\">\n <div [ngStyle]=\"{position: 'relative', height: '100%'}\">\n <div>\n @for (toast of toasts; track trackById(i, toast); let i = $index) { @if (toast.group?.parent) {} @else {\n <hot-toast\n [toast]=\"toast\"\n [offset]=\"calculateOffset(toast.id, toast.position)\"\n [toastRef]=\"toastRefs[i]\"\n [toastsAfter]=\"(toast.autoClose ? toasts.length : getVisibleToasts(toast.position).length) - 1 - i\"\n [defaultConfig]=\"defaultConfig\"\n [isShowingAllToasts]=\"isShowingAllToasts\"\n (showAllToasts)=\"showAllToasts($event)\"\n (height)=\"updateHeight($event, toast)\"\n (beforeClosed)=\"beforeClosed(toast)\"\n (afterClosed)=\"afterClosed($event)\"\n (toggleGroup)=\"toggleGroup($event)\"\n ></hot-toast>\n } }\n </div>\n </div>\n</div>\n" }]
}], propDecorators: { defaultConfig: [{
type: Input
}], hotToastComponentList: [{
type: ViewChildren,
args: [HotToastComponent]
}] } });
class ToastConfig {
constructor() {
/**
* Sets the reverse order for hot-toast stacking
*
* @default false
*/
this.reverseOrder = false;
/**
* Sets the number of toasts visible.
* 0 will set no limit.
* @default 5
* @since 6.1.0
*/
this.visibleToasts = 5;
/**
* Sets the type of stacking
* @default "vertical"
* @since 6.1.0
*/
this.stacking = 'vertical';
this.ariaLive = 'polite';
this.role = 'status';
this.position = 'top-center';
this.autoClose = true;
this.theme = 'toast';
this.attributes = {};
// key in ToastType
this.info = { content: '' };
this.success = { content: '' };
this.error = { content: '' };
this.loading = { content: '' };
this.blank = { content: '' };
this.warning = { content: '' };
}
}
const isFunction = (valOrFunction) => typeof valOrFunction === 'function';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isAngularComponent = (arg) => {
// `ɵcmp` is a component definition set by the Angular compiler.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return typeof arg === 'function' && !!arg.ɵcmp;
};
const resolveValueOrFunction = (valOrFunction, arg) => isAngularComponent(valOrFunction)
? valOrFunction
: isFunction(valOrFunction)
? valOrFunction(arg)
: valOrFunction;
class ToastPersistConfig {
constructor() {
/**
*In which storage id vs. counts should be stored
*
* @type {('local' | 'session')}
* @memberof ToastPersistConfig
* @default 'local'
*/
this.storage = 'local';
/**
*The key pattern to store object in storage. `${id}` in pattern is replaced with actual toast id.
*
* @type {('local' | 'session')}
* @memberof ToastPersistConfig
* @default 'ngxpert/hottoast-${id}'
*/
this.key = 'ngxpert/hototast-${id}';
/**
*The number of toasts allowed to show.
*
* @memberof ToastPersistConfig
* @default 1
*/
this.count = 1;
this.enabled = false;
}
}
class HotToastService {
static { this.nextId = 0; }
constructor() {
this._isInitialized = false;
this._defaultGlobalConfig = new ToastConfig();
this._defaultPersistConfig = new ToastPersistConfig();
this._viewService = inject(ViewService);
this._platformId = inject(PLATFORM_ID);
this._globalConfig = inject(ToastConfig, { optional: true });
if (this._globalConfig) {
this._defaultGlobalConfig = {
...this._defaultGlobalConfig,
...this._globalConfig,
};
}
}
get defaultConfig() {
return this._defaultGlobalConfig;
}
set defaultConfig(config) {
this._defaultGlobalConfig = {
...this._defaultGlobalConfig,
...config,
};
if (this._componentRef) {
this._componentRef.setInput('defaultConfig', this._defaultGlobalConfig);
}
}
/**
* Opens up an hot-toast without any pre-configurations
*
* @param message The message to show in the hot-toast.
* @param [options] Additional configuration options for the hot-toast.
* @param skipAttachToParent Only for internal usage. Setting this to true will not attach toast to it's parent.
* @returns
* @memberof HotToastService
*/
show(message, options, skipAttachToParent) {
const toast = this.createToast({
message: message || this._defaultGlobalConfig.blank.content,
type: options?.type ?? 'blank',
options: {
...this._defaultGlobalConfig,
...options,
},
skipAttachToParent,
});
return toast;
}
/**
* Opens up an hot-toast with pre-configurations for error state
*
* @param message The message to show in the hot-toast.
* @param [options] Additional configuration options for the hot-toast.
* @returns
* @memberof HotToastService
*/
error(message, options) {
const toast = this.createToast({
message: message || this._defaultGlobalConfig.error.content,
type: 'error',
options: {
...this._defaultGlobalConfig,
...this._defaultGlobalConfig?.error,
...options,
},
});
return toast;
}
/**
* Opens up an hot-toast with pre-configurations for success state
*
* @param message The message to show in the hot-toast.
* @param [options] Additional configuration options for the hot-toast.
* @returns
* @memberof HotToastService
*/
success(message, options) {
const toast = this.createToast({
message: message || this._defaultGlobalConfig.success.content,
type: 'success',
options: {
...this._defaultGlobalConfig,
...this._defaultGlobalConfig?.success,
...options,
},
});
return toast;
}
/**
* Opens up an hot-toast with pre-configurations for loading state
*
* @param message The message to show in the hot-toast.
* @param [options] Additional configuration options for the hot-toast.
* @returns
* @memberof HotToastService
*/
loading(message, options) {
const toast = this.createToast({
message: message || this._defaultGlobalConfig.loading.content,
type: 'loading',
options: {
...this._defaultGlobalConfig,
...this._defaultGlobalConfig?.loading,
...options,
},
});
return toast;
}
/**
* Opens up an hot-toast with pre-configurations for warning state
*
* @param message The message to show in the hot-toast.
* @param [options] Additional configuration options for the hot-toast.
* @returns
* @memberof HotToastService
*/
warning(message, options) {
const toast = this.createToast({
message: message || this._defaultGlobalConfig.warning.content,
type: 'warning',
options: {
...this._defaultGlobalConfig,
...this._defaultGlobalConfig?.warning,
...options,
},
});
return toast;
}
/**
* Opens up an hot-toast with pre-configurations for info state
*
* @param message The message to show in the hot-toast.
* @param [options] Additional configuration options for the hot-toast.
* @returns
* @memberof HotToastService
* @since 3.3.0
*/
info(message, options) {
const toast = this.createToast({
message: message || this._defaultGlobalConfig.info.content,
type: 'info',
options: {
...this._defaultGlobalConfig,
...this._defaultGlobalConfig?.info,
...options,
},
});
return toast;
}
/**
*
* Opens up an hot-toast with pre-configurations for loading initially and then changes state based on messages
*
* @template T Type of observable
* @param messages Messages for each state i.e. loading, success and error
* @returns
* @memberof HotToastService
*/
observe(messages) {
return (source) => {
let toastRef;
let start = 0;
const loadingContent = messages.loading ?? this._defaultGlobalConfig.loading?.content;
const successContent = messages.success ?? this._defaultGlobalConfig.success?.content;
const errorContent = messages.error ?? this._defaultGlobalConfig.error?.content;
return defer(() => {
if (loadingContent) {
toastRef = this.createLoadingToast(loadingContent);
start = Date.now();
}
return source.pipe(tap({
...(successContent && {
next: (val) => {
toastRef = this.createOrUpdateToast(messages, val, toastRef, 'success', start === 0 ? start : Date.now() - start);
},
}),
...(errorContent && {
error: (e) => {
toastRef = this.createOrUpdateToast(messages, e, toastRef, 'error', start === 0 ? start : Date.now() - start);
},
}),
}));
});
};
}
/**
* Closes the hot-toast
*
* @param [id] - ID of the toast
* @since 3.0.1 - If ID is not provided, all toasts will be closed
*/
close(id) {
if (this._componentRef) {
this._componentRef.ref.instance.closeToast(id);
}
}
/**
* Used for internal purpose only.
* Creates a container component and attaches it to document.body.
*/
init() {
if (isPlatformServer(this._platformId)) {
return;
}
this._componentRef = this._viewService
.createComponent(HotToastContainerComponent)
.setInput('defaultConfig', this._defaultGlobalConfig)
.appendTo(document.body);
}
createOrUpdateToast(messages, val, toastRef, type, diff) {
try {
let content = null;
let options = {};
({ content, options } = this.getContentAndOptions(type, messages[type] || (this._defaultGlobalConfig[type] ? this._defaultGlobalConfig[type].content : '')));
content = resolveValueOrFunction(content, val);
if (toastRef) {
if (options.data) {
toastRef.data = options.data;
}
toastRef.updateMessage(content);
const updatedOptions = {
type,
duration: diff + HOT_TOAST_DEFAULT_TIMEOUTS[type],
...options,
...(options.duration && { duration: diff + options.duration }),
};
toastRef.updateToast(updatedOptions);
}
else {
this.createToast({ message: content, type, options });
}
return toastRef;
}
catch (error) {
console.error(error);
}
}
createToast({ message, type, options, observableMessages, skipAttachToParent, }) {
if (!this._isInitialized) {
this._isInitialized = true;
this.init();
}
const id = options?.id ?? `toast-${HotToastService.nextId++}`;
if (!this.isDuplicate(id) &&
(!options.persist?.enabled || (options.persist?.enabled && this.handleStorageValue(id, options)))) {
const toast = {
ariaLive: options?.ariaLive ?? 'polite',
createdAt: Date.now(),
duration: options?.duration ?? HOT_TOAST_DEFAULT_TIMEOUTS[type],
id,
message,
role: options?.role ?? 'status',
type,
visible: true,
observableMessages: observableMessages ?? undefined,
...options,
};
return new HotToastRef(toast).appendTo(this._componentRef.ref.instance, skipAttachToParent);
}
}
/**
* Checks whether any toast with same id is present.
*
* @private
* @param id - Toast ID
*/
isDuplicate(id) {
return this._componentRef.ref.instance.hasToast(id);
}
/**
* Creates an entry in local or session storage with count ${defaultConfig.persist.count}, if not present.
* If present in storage, reduces the count
* and returns the count.
* Count can not be less than 0.
*/
handleStorageValue(id, options) {
let count = 1;
const persist = { ...this._defaultPersistConfig, ...options.persist };
const storage = persist.storage === 'local' ? localStorage : sessionStorage;
const key = persist.key.replace(/\${id}/g, id);
let item = storage.getItem(key);
if (item) {
item = parseInt(item, 10);
if (item > 0) {
count = item - 1;
}
else {
count = item;
}
}
else {
count = persist.count;
}
storage.setItem(key, count.toString());
return count;
}
getContentAndOptions(toastType, message) {
let content;
let options = {
...this._defaultGlobalConfig,
...this._defaultGlobalConfig[toastType],
};
// typeof message === 'object' won't work, cz TemplateRef's type is object
if (typeof message === 'string' || isTemplateRef(message) || isComponent(message)) {
content = message;
}
else {
let restOptions;
({ content, ...restOptions } = message);
options = { ...options, ...restOptions };
}
return { content, options };
}
createLoadingToast(messages) {
let content = null;
let options = {};
({ content, options } = this.getContentAndOptions('loading', messages));
return this.loading(content, options);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: HotToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: HotToastService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: HotToastService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
function provideHotToastConfig(config) {
return makeEnvironmentProviders([{ provide: ToastConfig, useValue: config }]);
}
class HotToastBuilder {
constructor(message, service) {
this.message = message;
this.service = service;
this.options = {};
this.groupChildren = [];
}
setOptions(options) {
this.options = { ...this.options, ...options };
return this;
}
addChild(child) {
this.groupChildren.push(child);
return this;
}
get afterGroupRefsAttached() {
return this.toastRef?.afterGroupRefsAttached;
}
addChildrenToOptions() {
if (this.groupChildren.length > 0) {
const children = this.groupChildren.map((child) => ({
options: {
message: child.message,
...child.options,
},
}));
this.options.group = {
...this.options.group,
children,
};
}
}
// Create method that creates but doesn't show the toast. Call show() to show the toast.
create(method = 'show') {
this.addChildrenToOptions();
this.toastRef = this.service[method](this.message, {
...this.options,
...{ visible: false },
});
return this.toastRef;
}
createToast(method) {
this.addChildrenToOptions();
this.toastRef = this.service[method](this.message, this.options);
return this.toastRef;
}
show() {
return this.createToast('show');
}
success() {
return this.createToast('success');
}
error() {
return this.createToast('error');
}
warning() {
return this.createToast('warning');
}
info() {
return this.createToast('info');
}
loading() {
return this.createToast('loading');
}
}
/*
* Public API Surface of hot-toast
*/
/**
* Generated bundle index. Do not edit.
*/
export { HotToastBuilder, HotToastRef, HotToastService, ToastConfig, ToastPersistConfig, provideHotToastConfig, resolveValueOrFunction };
//# sourceMappingURL=ngxpert-hot-toast.mjs.map