From c342de69adcf246a30e51f01c5a411a822756e3e Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Mon, 15 Apr 2024 00:50:43 +0530 Subject: [PATCH 1/9] feat: add grouping feature --- .../hot-toast-container.component.html | 4 +- .../hot-toast-container.component.ts | 21 +++++- .../hot-toast-group-item.component.html | 33 +++++++++ .../hot-toast-group-item.component.ts | 18 +++++ .../hot-toast/hot-toast.component.html | 24 ++++--- .../hot-toast/hot-toast.component.ts | 66 ++++++++++++++++- .../hot-toast/src/lib/hot-toast-ref.ts | 11 +-- .../hot-toast/src/lib/hot-toast.model.ts | 8 ++- .../hot-toast/src/lib/hot-toast.service.ts | 72 ++++++++++--------- .../src/styles/components/_hot-toast.scss | 6 ++ 10 files changed, 209 insertions(+), 54 deletions(-) create mode 100644 projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html create mode 100644 projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html index 8ea9b04..223cedd 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html @@ -1,7 +1,7 @@
- @for (toast of toasts; track trackById(i, toast); let i = $index) { + @for (toast of toasts; track trackById(i, toast); let i = $index) { @if (toast.group.parent) {} @else { - } + } }
diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts index 7799d7d..032b7dd 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts @@ -14,6 +14,7 @@ import { filter } from 'rxjs/operators'; import { Content } from '@ngneat/overview'; import { HotToastComponent } from '../hot-toast/hot-toast.component'; import { HOT_TOAST_DEPTH_SCALE, HOT_TOAST_DEPTH_SCALE_ADD, HOT_TOAST_MARGIN } from '../../constants'; +import { HotToastService } from '../../hot-toast.service'; @Component({ selector: 'hot-toast-container', @@ -36,7 +37,7 @@ export class HotToastContainerComponent { private onClosed$ = this._onClosed.asObservable(); - constructor(private cdr: ChangeDetectorRef) {} + constructor(private cdr: ChangeDetectorRef, private toastService: HotToastService) {} trackById(index: number, toast: Toast) { return toast.id; @@ -45,6 +46,7 @@ export class HotToastContainerComponent { getVisibleToasts(position: ToastPosition) { return this.toasts.filter((t) => t.visible && t.position === position); } + calculateOffset(toastId: string, position: ToastPosition) { const visibleToasts = this.getVisibleToasts(position); const index = visibleToasts.findIndex((toast) => toast.id === toastId); @@ -87,6 +89,22 @@ export class HotToastContainerComponent { this.cdr.detectChanges(); + const groupRefs: CreateHotToastRef[] = []; + + if (toast.group) { + if (toast.group.children) { + const items = toast.group.children; + groupRefs.push( + ...items.map((item) => { + item.options.group = { parent: ref }; + return this.toastService.show(item.options.message, item.options); + }) + ); + } else if (toast.group.parent) { + // TODO + } + } + return { dispose: () => { this.closeToast(toast.id); @@ -101,6 +119,7 @@ export class HotToastContainerComponent { this.cdr.detectChanges(); }, afterClosed: this.getAfterClosed(toast), + groupRefs, }; } diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html new file mode 100644 index 0000000..74ceb72 --- /dev/null +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html @@ -0,0 +1,33 @@ +
+
+
+ +
+
+
diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts new file mode 100644 index 0000000..07fb695 --- /dev/null +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Injector, NgZone, Renderer2 } from '@angular/core'; +import { HotToastComponent } from '../hot-toast/hot-toast.component'; +import { NgClass, NgStyle } from '@angular/common'; +import { AnimatedIconComponent } from '../animated-icon/animated-icon.component'; +import { IndicatorComponent } from '../indicator/indicator.component'; + +@Component({ + selector: 'hot-toast-group-item', + templateUrl: 'hot-toast-group-item.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgClass, NgStyle, AnimatedIconComponent, IndicatorComponent], +}) +export class HotToastGroupItemComponent extends HotToastComponent { + constructor(injector: Injector, renderer: Renderer2, ngZone: NgZone, cdr: ChangeDetectorRef) { + super(injector, renderer, ngZone, cdr); + } +} diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html index d486a87..4ec6c76 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html @@ -34,16 +34,24 @@ + - @if (toast.dismissible) { - + @if (toast.group?.children) { +
+ @for (item of childGroupToasts; track $index) { + }
+ } diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts index e2f61a7..fc6b392 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts @@ -1,6 +1,7 @@ import { AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, EventEmitter, @@ -22,25 +23,28 @@ import { ENTER_ANIMATION_DURATION, EXIT_ANIMATION_DURATION, HOT_TOAST_DEPTH_SCALE, + HOT_TOAST_MARGIN, } from '../../constants'; import { HotToastRef } from '../../hot-toast-ref'; -import { CreateHotToastRef, HotToastClose, Toast, ToastConfig } from '../../hot-toast.model'; +import { CreateHotToastRef, HotToastClose, Toast, ToastConfig, ToastPosition } from '../../hot-toast.model'; import { animate } from '../../utils'; import { IndicatorComponent } from '../indicator/indicator.component'; import { AnimatedIconComponent } from '../animated-icon/animated-icon.component'; +import { HotToastGroupItemComponent } from '../hot-toast-group-item/hot-toast-group-item.component'; @Component({ selector: 'hot-toast', templateUrl: 'hot-toast.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, DynamicViewDirective, IndicatorComponent, AnimatedIconComponent], + imports: [CommonModule, DynamicViewDirective, IndicatorComponent, AnimatedIconComponent, HotToastGroupItemComponent], }) export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { @Input() toast: Toast; @Input() offset = 0; @Input() defaultConfig: ToastConfig; @Input() toastRef: CreateHotToastRef; + private _toastsAfter = 0; get toastsAfter() { return this._toastsAfter; @@ -62,6 +66,7 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh } } } + @Input() isShowingAllToasts = false; @Output() height = new EventEmitter(); @@ -78,7 +83,12 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh private unlisteners: VoidFunction[] = []; private softClosed = false; - constructor(private injector: Injector, private renderer: Renderer2, private ngZone: NgZone) {} + constructor( + private injector: Injector, + private renderer: Renderer2, + private ngZone: NgZone, + private cdr: ChangeDetectorRef + ) {} get toastBarBaseHeight() { return this.toastBarBase.nativeElement.offsetHeight; @@ -144,6 +154,20 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh return typeof this.toast.icon === 'string'; } + get childGroupToasts() { + return this.toast.group.children.map((t) => t.options) ?? []; + } + set childGroupToasts(value) { + this.toast.group.children = value.map((t) => ({ options: t })) ?? []; + } + + get childGroupToastRefs() { + return this.toastRef.groupRefs ?? []; + } + set childGroupToastRefs(value) { + this.toastRef.groupRefs = value; + } + ngOnChanges(changes: SimpleChanges): void { if (changes.toast && !changes.toast.firstChange && changes.toast.currentValue?.message) { requestAnimationFrame(() => { @@ -257,4 +281,40 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh this.renderer.setAttribute(this.toastBarBase.nativeElement, key, value); } } + + getVisibleToasts(position: ToastPosition) { + return this.childGroupToasts.filter((t) => t.visible && t.position === position); + } + + calculateOffset(toastId: string, position: ToastPosition) { + 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) => { + return this.defaultConfig.visibleToasts !== 0 && i < visibleToasts.length - this.defaultConfig.visibleToasts + ? 0 + : acc + (t.height || 0) + HOT_TOAST_MARGIN; + }, 0) + : 0; + return offset; + } + + updateHeight(height: number, toast: Toast) { + toast.height = height; + this.cdr.detectChanges(); + } + + beforeClosedGroupItem(toast: Toast) { + toast.visible = false; + } + + afterClosedGroupItem(closeToast: HotToastClose) { + const toastIndex = this.childGroupToasts.findIndex((t) => t.id === closeToast.id); + if (toastIndex > -1) { + this.childGroupToasts = this.childGroupToasts.filter((t) => t.id !== closeToast.id); + this.childGroupToastRefs = this.childGroupToastRefs.filter((t) => t.getToast().id !== closeToast.id); + this.cdr.detectChanges(); + } + } } diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts index e3623f2..b8b5d52 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts @@ -1,14 +1,15 @@ import { Content } from '@ngneat/overview'; -import { Observable, race, Subject } from 'rxjs'; +import { defer, Observable, race, Subject } from 'rxjs'; // This should be a `type` import since it causes `ng-packagr` compilation to fail because of a cyclic dependency. import type { HotToastContainerComponent } from './components/hot-toast-container/hot-toast-container.component'; -import { HotToastClose, Toast, UpdateToastOptions, HotToastRefProps, DefaultDataType } from './hot-toast.model'; +import { HotToastClose, Toast, UpdateToastOptions, HotToastRefProps, DefaultDataType, CreateHotToastRef } from './hot-toast.model'; export class HotToastRef implements HotToastRefProps { updateMessage: (message: Content) => void; updateToast: (options: UpdateToastOptions) => void; afterClosed: Observable; + groupRefs: CreateHotToastRef[] = []; private _dispose: () => void; @@ -33,16 +34,18 @@ export class HotToastRef implements HotToastRefProps return this.toast; } - /**Used for internal purpose + /** + * Used for internal purpose * Attach ToastRef to container */ appendTo(container: HotToastContainerComponent) { - const { dispose, updateMessage, updateToast, afterClosed } = container.addToast(this); + const { dispose, updateMessage, updateToast, afterClosed, groupRefs } = container.addToast(this); this.dispose = dispose; this.updateMessage = updateMessage; this.updateToast = updateToast; this.afterClosed = race(this._onClosed.asObservable(), afterClosed); + this.groupRefs = groupRefs; return this; } diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts index 36c683d..4a34de4 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts @@ -1,6 +1,7 @@ import { Component, Injector } from '@angular/core'; import { Content } from '@ngneat/overview'; import { Observable } from 'rxjs'; +import { HotToastRef } from './hot-toast-ref'; export type ToastStacking = 'vertical' | 'depth'; @@ -201,6 +202,8 @@ export interface Toast { * @memberof Toast */ data?: DataType; + + group?: { children?: { options: Toast }[], parent?: CreateHotToastRef }; } export type ToastOptions = Partial< @@ -223,6 +226,7 @@ export type ToastOptions = Partial< | 'injector' | 'data' | 'attributes' + | 'group' > >; @@ -271,12 +275,14 @@ export interface HotToastRefProps { updateToast: (options: UpdateToastOptions) => void; /** Observable for notifying the user that the toast has been closed. */ afterClosed: Observable; + afterGroupItemClosed?: Observable; /**Closes the toast */ close: (closeData?: { dismissedByAction: boolean }) => void; /** * @since 2.0.0 */ data: DataType; + groupRefs: CreateHotToastRef[]; } /** Event that is emitted when a snack bar is dismissed. */ @@ -318,7 +324,7 @@ export class ToastPersistConfig { export type AddToastRef = Pick< HotToastRefProps, - 'afterClosed' | 'dispose' | 'updateMessage' | 'updateToast' + 'afterClosed' | 'dispose' | 'updateMessage' | 'updateToast' | 'groupRefs' >; export type CreateHotToastRef = Omit, 'appendTo'>, 'dispose'>; diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts index 891ff55..2887caf 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts @@ -29,32 +29,32 @@ export class HotToastService implements HotToastServiceMethods { private _isInitialized = false; private _componentRef: CompRef; - private _defaultConfig = new ToastConfig(); + private _defaultGlobalConfig = new ToastConfig(); private _defaultPersistConfig = new ToastPersistConfig(); constructor( private _viewService: ViewService, @Inject(PLATFORM_ID) private platformId: string, - @Optional() config: ToastConfig + @Optional() globalConfig: ToastConfig ) { - if (config) { - this._defaultConfig = { - ...this._defaultConfig, - ...config, + if (globalConfig) { + this._defaultGlobalConfig = { + ...this._defaultGlobalConfig, + ...globalConfig, }; } } get defaultConfig() { - return this._defaultConfig; + return this._defaultGlobalConfig; } set defaultConfig(config: ToastConfig) { - this._defaultConfig = { - ...this._defaultConfig, + this._defaultGlobalConfig = { + ...this._defaultGlobalConfig, ...config, }; if (this._componentRef) { - this._componentRef.setInput('defaultConfig', this._defaultConfig); + this._componentRef.setInput('defaultConfig', this._defaultGlobalConfig); } } @@ -67,8 +67,8 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ show(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultConfig.blank.content, 'blank', { - ...this._defaultConfig, + const toast = this.createToast(message || this._defaultGlobalConfig.blank.content, 'blank', { + ...this._defaultGlobalConfig, ...options, }); @@ -84,9 +84,9 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ error(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultConfig.error.content, 'error', { - ...this._defaultConfig, - ...this._defaultConfig?.error, + const toast = this.createToast(message || this._defaultGlobalConfig.error.content, 'error', { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.error, ...options, }); @@ -102,9 +102,9 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ success(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultConfig.success.content, 'success', { - ...this._defaultConfig, - ...this._defaultConfig?.success, + const toast = this.createToast(message || this._defaultGlobalConfig.success.content, 'success', { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.success, ...options, }); @@ -120,9 +120,9 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ loading(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultConfig.loading.content, 'loading', { - ...this._defaultConfig, - ...this._defaultConfig?.loading, + const toast = this.createToast(message || this._defaultGlobalConfig.loading.content, 'loading', { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.loading, ...options, }); @@ -138,9 +138,9 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ warning(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultConfig.warning.content, 'warning', { - ...this._defaultConfig, - ...this._defaultConfig?.warning, + const toast = this.createToast(message || this._defaultGlobalConfig.warning.content, 'warning', { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.warning, ...options, }); @@ -157,9 +157,9 @@ export class HotToastService implements HotToastServiceMethods { * @since 3.3.0 */ info(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultConfig.info.content, 'info', { - ...this._defaultConfig, - ...this._defaultConfig?.info, + const toast = this.createToast(message || this._defaultGlobalConfig.info.content, 'info', { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.info, ...options, }); @@ -175,14 +175,16 @@ export class HotToastService implements HotToastServiceMethods { * @returns * @memberof HotToastService */ - observe(messages: ObservableMessages): (source: Observable) => Observable { + observe( + messages: ObservableMessages + ): (source: Observable) => Observable { return (source) => { let toastRef: CreateHotToastRef; let start = 0; - const loadingContent = messages.loading ?? this._defaultConfig.loading?.content; - const successContent = messages.success ?? this._defaultConfig.success?.content; - const errorContent = messages.error ?? this._defaultConfig.error?.content; + 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) { @@ -241,7 +243,7 @@ export class HotToastService implements HotToastServiceMethods { } this._componentRef = this._viewService .createComponent(HotToastContainerComponent) - .setInput('defaultConfig', this._defaultConfig) + .setInput('defaultConfig', this._defaultGlobalConfig) .appendTo(document.body); } @@ -257,7 +259,7 @@ export class HotToastService implements HotToastServiceMethods { let options: ToastOptions = {}; ({ content, options } = this.getContentAndOptions( type, - messages[type] || (this._defaultConfig[type] ? this._defaultConfig[type].content : '') + messages[type] || (this._defaultGlobalConfig[type] ? this._defaultGlobalConfig[type].content : '') )); content = resolveValueOrFunction(content, val); if (toastRef) { @@ -362,8 +364,8 @@ export class HotToastService implements HotToastServiceMethods { ): { options: ToastOptions; content: Content | ValueOrFunction } { let content: Content | ValueOrFunction; let options: ToastOptions = { - ...this._defaultConfig, - ...this._defaultConfig[toastType], + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig[toastType], }; // typeof message === 'object' won't work, cz TemplateRef's type is object diff --git a/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss b/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss index da8e8dd..544e47b 100644 --- a/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss +++ b/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss @@ -99,6 +99,12 @@ pointer-events: all; } +.hot-toast-bar-base-group { + background-color: var(--hot-toast-group-bg, #fff); + --hot-toast-border-radius: 0; + --hot-toast-shadow: none; +} + @keyframes hotToastEnterAnimationNegative { 0% { opacity: 0.5; From bc32842f04b7f997cbefb98d2ad217941652b2bb Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Wed, 17 Apr 2024 00:10:58 +0530 Subject: [PATCH 2/9] feat: post grouping --- .../hot-toast-container.component.html | 3 +- .../hot-toast-container.component.ts | 122 ++++++-- .../hot-toast-group-item.component.html | 14 +- .../hot-toast-group-item.component.ts | 264 +++++++++++++++++- .../hot-toast/hot-toast.component.html | 45 ++- .../hot-toast/hot-toast.component.ts | 101 +++++-- .../indicator/indicator.component.html | 3 +- .../hot-toast/src/lib/hot-toast-ref.ts | 34 ++- .../hot-toast/src/lib/hot-toast.model.ts | 78 +++++- .../hot-toast/src/lib/hot-toast.service.ts | 101 ++++--- .../src/styles/components/_hot-toast.scss | 71 ++++- src/app/app.component.html | 226 ++++++++------- src/app/app.component.ts | 181 ++++++++++-- .../core/services/code-highlight.service.ts | 2 + .../sections/example/example.component.html | 10 - src/app/sections/example/example.component.ts | 44 ++- .../sections/grouping/grouping.component.html | 111 ++++++++ .../sections/grouping/grouping.component.scss | 35 +++ .../sections/grouping/grouping.component.ts | 167 +++++++++++ src/app/sections/grouping/snippets.ts | 228 +++++++++++++++ .../sections/position/position.component.html | 4 +- .../reverse-order.component.html | 4 +- .../sections/stacking/stacking.component.html | 4 +- .../components/code/code.component.html | 2 +- .../components/code/code.component.scss | 12 + .../emoji-button/emoji-button.component.html | 1 + .../emoji-button/emoji-button.component.ts | 1 + src/styles.scss | 10 + src/styles/grouping.css | 7 + 29 files changed, 1611 insertions(+), 274 deletions(-) create mode 100644 src/app/sections/grouping/grouping.component.html create mode 100644 src/app/sections/grouping/grouping.component.scss create mode 100644 src/app/sections/grouping/grouping.component.ts create mode 100644 src/app/sections/grouping/snippets.ts create mode 100644 src/styles/grouping.css diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html index 223cedd..2da6582 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.html @@ -1,7 +1,7 @@
- @for (toast of toasts; track trackById(i, toast); let i = $index) { @if (toast.group.parent) {} @else { + @for (toast of toasts; track trackById(i, toast); let i = $index) { @if (toast.group?.parent) {} @else { } }
diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts index 032b7dd..fa95dff 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts @@ -8,9 +8,11 @@ import { UpdateToastOptions, AddToastRef, CreateHotToastRef, + HotToastGroupEvent, + HotToastGroupChild, } from '../../hot-toast.model'; import { HotToastRef } from '../../hot-toast-ref'; -import { filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { Content } from '@ngneat/overview'; import { HotToastComponent } from '../hot-toast/hot-toast.component'; import { HOT_TOAST_DEPTH_SCALE, HOT_TOAST_DEPTH_SCALE_ADD, HOT_TOAST_MARGIN } from '../../constants'; @@ -35,7 +37,15 @@ export class HotToastContainerComponent { /** Subject for notifying the user that the toast has been closed. */ private _onClosed = new Subject(); + /** Subject for notifying the user that the toast has been expanded or collapsed. */ + private _onGroupToggle = new Subject(); + + /** Subject for notifying the user that the group refs have been attached to toast. */ + private _onGroupRefAttached = new Subject<{ groupRefs: CreateHotToastRef[]; id: string }>(); + private onClosed$ = this._onClosed.asObservable(); + private onGroupToggle$ = this._onGroupToggle.asObservable(); + private onGroupRefAttached$ = this._onGroupRefAttached.asObservable(); constructor(private cdr: ChangeDetectorRef, private toastService: HotToastService) {} @@ -44,9 +54,13 @@ export class HotToastContainerComponent { } getVisibleToasts(position: ToastPosition) { - return this.toasts.filter((t) => t.visible && t.position === position); + return this.toasts.filter((t) => t.group?.parent === undefined && t.visible && t.position === position); } - + + get unGroupedToasts() { + return this.toasts.filter((t) => t.group?.parent === undefined); + } + calculateOffset(toastId: string, position: ToastPosition) { const visibleToasts = this.getVisibleToasts(position); const index = visibleToasts.findIndex((toast) => toast.id === toastId); @@ -71,14 +85,14 @@ export class HotToastContainerComponent { this.cdr.detectChanges(); } - addToast(ref: HotToastRef): AddToastRef { + addToast(ref: HotToastRef, skipAttachToParent?: boolean): AddToastRef { this.toastRefs.push(ref); const toast = ref.getToast(); this.toasts.push(ref.getToast()); - if (this.defaultConfig.visibleToasts !== 0 && this.toasts.length > this.defaultConfig.visibleToasts) { + 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) { @@ -89,21 +103,7 @@ export class HotToastContainerComponent { this.cdr.detectChanges(); - const groupRefs: CreateHotToastRef[] = []; - - if (toast.group) { - if (toast.group.children) { - const items = toast.group.children; - groupRefs.push( - ...items.map((item) => { - item.options.group = { parent: ref }; - return this.toastService.show(item.options.message, item.options); - }) - ); - } else if (toast.group.parent) { - // TODO - } - } + this.attachGroupRefs(toast, ref, skipAttachToParent); return { dispose: () => { @@ -119,10 +119,73 @@ export class HotToastContainerComponent { this.cdr.detectChanges(); }, afterClosed: this.getAfterClosed(toast), - groupRefs, + afterGroupToggled: this.getAfterGroupToggled(toast), + afterGroupRefsAttached: this.getAfterGroupRefsAttached(toast).pipe(map((v) => v.groupRefs)), }; } + private async attachGroupRefs( + toast: Toast, + ref: HotToastRef, + skipAttachToParent?: boolean + ) { + let groupRefs: CreateHotToastRef[] = []; + + 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] as { groupRefs: CreateHotToastRef[] }).groupRefs = groupRefs; + + this.cdr.detectChanges(); + 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.detectChanges(); + + this._onGroupRefAttached.next({ groupRefs, id: parentToast.id }); + } + } + } + } + + private createGroupRefs(toast: Toast, ref: HotToastRef) { + const skipAttachToParent = true; + return new Promise[]>((resolve) => { + const items = toast.group.children; + const allPromises: Promise>[] = 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(() => { + const itemRef = this.toastService.show(item.options.message, item.options, skipAttachToParent); + innerResolve(itemRef); + }); + }); + }); + Promise.all(allPromises).then((refs) => resolve(refs)); + }); + } + closeToast(id?: string) { if (id) { const comp = this.hotToastComponentList.find((item) => item.toast.id === id); @@ -148,6 +211,15 @@ export class HotToastContainerComponent { } } + toggleGroup(groupEvent: HotToastGroupEvent) { + const toastIndex = this.toastRefs.findIndex((t) => t.getToast().id === groupEvent.id); + if (toastIndex > -1) { + this._onGroupToggle.next(groupEvent); + (this.toastRefs[toastIndex] as { groupExpanded: boolean }).groupExpanded = groupEvent.event === 'expand'; + this.cdr.detectChanges(); + } + } + hasToast(id: string) { return this.toasts.findIndex((t) => t.id === id) > -1; } @@ -160,6 +232,14 @@ export class HotToastContainerComponent { return this.onClosed$.pipe(filter((v) => v.id === toast.id)); } + private getAfterGroupToggled(toast: Toast) { + return this.onGroupToggle$.pipe(filter((v) => v.id === toast.id)); + } + + private getAfterGroupRefsAttached(toast: Toast) { + return this.onGroupRefAttached$.pipe(filter((v) => v.id === toast.id)); + } + private updateToasts(toast: Toast, options?: UpdateToastOptions) { this.toasts = this.toasts.map((t) => ({ ...t, ...(t.id === toast.id && { ...toast, ...options }) })); } diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html index 74ceb72..691a5ec 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html @@ -2,7 +2,7 @@ class="hot-toast-bar-base-container" [ngStyle]="containerPositionStyle" [ngClass]="'hot-toast-theme-' + toast.theme" - [style.--hot-toast-scale]="1" + [style.--hot-toast-scale]="scale" [style.--hot-toast-translate-y]="translateY" >
@@ -28,6 +28,18 @@ }
+
+ +
+ @if (toast.dismissible) { + + }
diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts index 07fb695..b0fa100 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.ts @@ -1,18 +1,270 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Injector, NgZone, Renderer2 } from '@angular/core'; -import { HotToastComponent } from '../hot-toast/hot-toast.component'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Injector, + Input, + NgZone, + Output, + Renderer2, + SimpleChanges, + ViewChild, +} from '@angular/core'; import { NgClass, NgStyle } from '@angular/common'; import { AnimatedIconComponent } from '../animated-icon/animated-icon.component'; import { IndicatorComponent } from '../indicator/indicator.component'; +import { DynamicViewDirective, isComponent, isTemplateRef } from '@ngneat/overview'; +import { ENTER_ANIMATION_DURATION, EXIT_ANIMATION_DURATION, HOT_TOAST_DEPTH_SCALE } from '../../constants'; +import { HotToastRef } from '../../hot-toast-ref'; +import { Toast, ToastConfig, CreateHotToastRef, HotToastClose, HotToastGroupEvent } from '../../hot-toast.model'; +import { animate } from '../../utils'; @Component({ selector: 'hot-toast-group-item', templateUrl: 'hot-toast-group-item.component.html', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgClass, NgStyle, AnimatedIconComponent, IndicatorComponent], + imports: [NgClass, NgStyle, AnimatedIconComponent, IndicatorComponent, DynamicViewDirective], }) -export class HotToastGroupItemComponent extends HotToastComponent { - constructor(injector: Injector, renderer: Renderer2, ngZone: NgZone, cdr: ChangeDetectorRef) { - super(injector, renderer, ngZone, cdr); +export class HotToastGroupItemComponent { + @Input() toast: Toast; + @Input() offset = 0; + @Input() defaultConfig: ToastConfig; + @Input() toastRef: CreateHotToastRef; + + private _toastsAfter = 0; + get toastsAfter() { + return this._toastsAfter; + } + @Input() + set toastsAfter(value) { + this._toastsAfter = value; + } + + @Input() isShowingAllToasts = false; + + @Output() height = new EventEmitter(); + @Output() beforeClosed = new EventEmitter(); + @Output() afterClosed = new EventEmitter(); + @Output() showAllToasts = new EventEmitter(); + @Output() toggleGroup = new EventEmitter(); + + @ViewChild('hotToastBarBase') protected toastBarBase: ElementRef; + + isManualClose = false; + context: Record; + toastComponentInjector: Injector; + + private unlisteners: VoidFunction[] = []; + protected softClosed = false; + + constructor( + protected injector: Injector, + protected renderer: Renderer2, + protected ngZone: NgZone, + protected cdr: ChangeDetectorRef + ) {} + + 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 toastBarBaseStyles() { + const enterAnimation = `hotToastEnterAnimation${ + this.top ? 'Negative' : 'Positive' + } ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; + + 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)`; + + const animation = this.toast.autoClose ? `${enterAnimation}, ${exitAnimation}` : enterAnimation; + + return { ...this.toast.style, animation }; + } + + get isIconString() { + return typeof this.toast.icon === 'string'; + } + + get groupChildrenToastRefs() { + return this.toastRef.groupRefs; + } + set groupChildrenToastRefs(value: CreateHotToastRef[]) { + (this.toastRef as { groupRefs: CreateHotToastRef[] }).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: SimpleChanges): void { + 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, + }); + } + } + + 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); + }); + + // 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: AnimationEvent) => { + if (this.isExitAnimation(event)) { + this.ngZone.run(() => this.beforeClosed.emit()); + } + }), + this.renderer.listen(nativeElement, 'animationend', (event: AnimationEvent) => { + if (this.isExitAnimation(event)) { + this.ngZone.run(() => this.afterClosed.emit({ dismissedByAction: this.isManualClose, id: this.toast.id })); + } + }) + ); + }); + + 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(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(nativeElement, softEnterAnimation); + this.softClosed = false; + } + + close() { + this.isManualClose = true; + + const exitAnimation = `hotToastExitAnimation${ + this.top ? 'Negative' : 'Positive' + } ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`; + + const nativeElement = this.toastBarBase.nativeElement; + + animate(nativeElement, exitAnimation); + } + + handleMouseEnter() { + this.showAllToasts.emit(true); + } + handleMouseLeave() { + this.showAllToasts.emit(false); + } + + ngOnDestroy() { + this.close(); + while (this.unlisteners.length) { + this.unlisteners.pop()(); + } + } + + private isExitAnimation(ev: AnimationEvent) { + return ev.animationName.includes('hotToastExitAnimation'); + } + + private setToastAttributes() { + const toastAttributes: Record = 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); } } diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html index 4ec6c76..de4e389 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html @@ -5,7 +5,12 @@ [style.--hot-toast-scale]="scale" [style.--hot-toast-translate-y]="translateY" > -
+
-
- -
+
+ + @if (toast.group?.expandAndCollapsible && toast.group?.children && visibleToasts.length > 0) { + + } @if (toast.dismissible) { + + }
- @if (toast.group?.children) { -
- @for (item of childGroupToasts; track $index) { +
+ @for (item of groupChildrenToasts; track item.id) { }
- }
diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts index fc6b392..253a05d 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + DoCheck, ElementRef, EventEmitter, Injector, @@ -19,14 +20,9 @@ import { import { CommonModule } from '@angular/common'; import { DynamicViewDirective, isComponent, isTemplateRef } from '@ngneat/overview'; -import { - ENTER_ANIMATION_DURATION, - EXIT_ANIMATION_DURATION, - HOT_TOAST_DEPTH_SCALE, - HOT_TOAST_MARGIN, -} from '../../constants'; +import { ENTER_ANIMATION_DURATION, EXIT_ANIMATION_DURATION, HOT_TOAST_DEPTH_SCALE } from '../../constants'; import { HotToastRef } from '../../hot-toast-ref'; -import { CreateHotToastRef, HotToastClose, Toast, ToastConfig, ToastPosition } from '../../hot-toast.model'; +import { CreateHotToastRef, HotToastClose, HotToastGroupEvent, Toast, ToastConfig } from '../../hot-toast.model'; import { animate } from '../../utils'; import { IndicatorComponent } from '../indicator/indicator.component'; import { AnimatedIconComponent } from '../animated-icon/animated-icon.component'; @@ -39,7 +35,7 @@ import { HotToastGroupItemComponent } from '../hot-toast-group-item/hot-toast-gr standalone: true, imports: [CommonModule, DynamicViewDirective, IndicatorComponent, AnimatedIconComponent, HotToastGroupItemComponent], }) -export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { +export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges, DoCheck { @Input() toast: Toast; @Input() offset = 0; @Input() defaultConfig: ToastConfig; @@ -73,6 +69,7 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh @Output() beforeClosed = new EventEmitter(); @Output() afterClosed = new EventEmitter(); @Output() showAllToasts = new EventEmitter(); + @Output() toggleGroup = new EventEmitter(); @ViewChild('hotToastBarBase') private toastBarBase: ElementRef; @@ -82,6 +79,7 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh private unlisteners: VoidFunction[] = []; private softClosed = false; + private groupRefs: CreateHotToastRef[] = []; constructor( private injector: Injector, @@ -154,25 +152,47 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh return typeof this.toast.icon === 'string'; } - get childGroupToasts() { - return this.toast.group.children.map((t) => t.options) ?? []; + get groupChildrenToastRefs() { + return this.groupRefs; + } + set groupChildrenToastRefs(value: CreateHotToastRef[]) { + this.groupRefs = value; + + // maybe below will prevent execution in ngDoCheck? + (this.toastRef as { groupRefs: CreateHotToastRef[] }).groupRefs = value; + } + + get groupChildrenToasts() { + return this.groupChildrenToastRefs.map((ref) => ref.getToast()); } - set childGroupToasts(value) { - this.toast.group.children = value.map((t) => ({ options: t })) ?? []; + + get groupHeight() { + return this.visibleToasts + .slice(-this.defaultConfig.visibleToasts) + .map((t) => t.height) + .reduce((prev, curr) => prev + curr, 0); } - get childGroupToastRefs() { - return this.toastRef.groupRefs ?? []; + get isExpanded() { + return this.toastRef.groupExpanded; } - set childGroupToastRefs(value) { - this.toastRef.groupRefs = value; + + 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.detectChanges(); + + this.emiHeightWithGroup(this.isExpanded); + } } ngOnChanges(changes: SimpleChanges): void { if (changes.toast && !changes.toast.firstChange && changes.toast.currentValue?.message) { - requestAnimationFrame(() => { - this.height.emit(this.toastBarBase.nativeElement.offsetHeight); - }); + this, this.emiHeightWithGroup(this.isExpanded); } } @@ -282,19 +302,15 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh } } - getVisibleToasts(position: ToastPosition) { - return this.childGroupToasts.filter((t) => t.visible && t.position === position); - } - - calculateOffset(toastId: string, position: ToastPosition) { - const visibleToasts = this.getVisibleToasts(position); + calculateOffset(toastId: string) { + 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) + HOT_TOAST_MARGIN; + : acc + (t.height || 0); }, 0) : 0; return offset; @@ -307,14 +323,41 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh beforeClosedGroupItem(toast: Toast) { toast.visible = false; + this.cdr.detectChanges(); + if (this.visibleToasts.length === 0 && this.isExpanded) { + this.toggleToastGroup(); + } else { + this.emiHeightWithGroup(this.isExpanded); + } } afterClosedGroupItem(closeToast: HotToastClose) { - const toastIndex = this.childGroupToasts.findIndex((t) => t.id === closeToast.id); + const toastIndex = this.groupChildrenToasts.findIndex((t) => t.id === closeToast.id); if (toastIndex > -1) { - this.childGroupToasts = this.childGroupToasts.filter((t) => t.id !== closeToast.id); - this.childGroupToastRefs = this.childGroupToastRefs.filter((t) => t.getToast().id !== closeToast.id); + this.groupChildrenToastRefs = this.groupChildrenToastRefs.filter((t) => t.getToast().id !== closeToast.id); this.cdr.detectChanges(); } } + + toggleToastGroup() { + const event = this.isExpanded ? 'collapse' : 'expand'; + this.toggleGroup.emit({ + byAction: true, + event, + id: this.toast.id, + }); + this.emiHeightWithGroup(event === 'expand'); + } + + private emiHeightWithGroup(isExpanded: boolean) { + if (isExpanded) { + requestAnimationFrame(() => { + this.height.emit(this.toastBarBase.nativeElement.offsetHeight + this.groupHeight); + }); + } else { + requestAnimationFrame(() => { + this.height.emit(this.toastBarBase.nativeElement.offsetHeight); + }); + } + } } diff --git a/projects/ngxpert/hot-toast/src/lib/components/indicator/indicator.component.html b/projects/ngxpert/hot-toast/src/lib/components/indicator/indicator.component.html index b932207..bfa1c22 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/indicator/indicator.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/indicator/indicator.component.html @@ -1,7 +1,8 @@ @if (type !== 'blank') {
+ @if (type === 'loading') { - @if (type !== 'loading') { + } @if (type !== 'loading') {
@switch (type) { @case ('error') { diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts index b8b5d52..fcd3304 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast-ref.ts @@ -3,19 +3,33 @@ import { defer, Observable, race, Subject } from 'rxjs'; // This should be a `type` import since it causes `ng-packagr` compilation to fail because of a cyclic dependency. import type { HotToastContainerComponent } from './components/hot-toast-container/hot-toast-container.component'; -import { HotToastClose, Toast, UpdateToastOptions, HotToastRefProps, DefaultDataType, CreateHotToastRef } from './hot-toast.model'; +import { + HotToastClose, + Toast, + UpdateToastOptions, + HotToastRefProps, + DefaultDataType, + CreateHotToastRef, + HotToastGroupEvent, +} from './hot-toast.model'; export class HotToastRef implements HotToastRefProps { updateMessage: (message: Content) => void; updateToast: (options: UpdateToastOptions) => void; afterClosed: Observable; + afterGroupToggled: Observable; + afterGroupRefsAttached: Observable[]>; groupRefs: CreateHotToastRef[] = []; + groupExpanded = false; private _dispose: () => void; /** Subject for notifying the user that the toast has been closed. */ private _onClosed = new Subject(); + /** Subject for notifying the user that the toast has been closed. */ + private _onGroupToggle = new Subject(); + constructor(private toast: Toast) {} set data(data: DataType) { @@ -38,14 +52,16 @@ export class HotToastRef implements HotToastRefProps * Used for internal purpose * Attach ToastRef to container */ - appendTo(container: HotToastContainerComponent) { - const { dispose, updateMessage, updateToast, afterClosed, groupRefs } = container.addToast(this); + appendTo(container: HotToastContainerComponent, skipAttachToParent?: boolean) { + 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.groupRefs = groupRefs; + this.afterGroupToggled = race(this._onGroupToggle.asObservable(), afterGroupToggled); + this.afterGroupRefsAttached = afterGroupRefsAttached; return this; } @@ -57,8 +73,18 @@ export class HotToastRef implements HotToastRefProps * @memberof HotToastRef */ close(closeData: { dismissedByAction: boolean } = { 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: boolean } = { byAction: false }) { + this.groupExpanded = !this.groupExpanded; + this._onGroupToggle.next({ + byAction: eventData.byAction, + id: this.toast.id, + event: this.groupExpanded ? 'expand' : 'collapse', + }); + } } diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts index 4a34de4..8d7d6b1 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts @@ -85,6 +85,13 @@ export type ToastRole = 'status' | 'alert'; export type ToastAriaLive = 'assertive' | 'off' | 'polite'; +export interface HotToastGroupChild { + options: ToastOptions & { + type?: ToastType; + message: Content; + }; +} + export interface Toast { type: ToastType; @@ -203,7 +210,34 @@ export interface Toast { */ data?: DataType; - group?: { children?: { options: Toast }[], parent?: CreateHotToastRef }; + /** + * @since 1.1.0 + */ + group?: { + /** + * Show group expand/collapse button in hot-toast + * + * @default false + */ + expandAndCollapsible?: boolean; + + /**Extra styles to apply for expand/collapse button */ + btnStyle?: any; + + /**Extra CSS classes to be added to the hot toast container. */ + className?: string; + + /** + * Child items to render as grouped + */ + children?: HotToastGroupChild[]; + + /** + * Parent toast ref to be passed with newly created toast, + * and if it needs to grouped under an existing toast + */ + parent?: CreateHotToastRef; + }; } export type ToastOptions = Partial< @@ -275,23 +309,55 @@ export interface HotToastRefProps { updateToast: (options: UpdateToastOptions) => void; /** Observable for notifying the user that the toast has been closed. */ afterClosed: Observable; - afterGroupItemClosed?: Observable; + + /** Observable for notifying the user that the group has been toggled. */ + afterGroupToggled: Observable; + + /** Observable for notifying the user that all the toastRefs for groups has been attached. */ + afterGroupRefsAttached: Observable[]>; + /**Closes the toast */ close: (closeData?: { dismissedByAction: boolean }) => void; + /** * @since 2.0.0 */ data: DataType; - groupRefs: CreateHotToastRef[]; + + /** + * List of group toast refs + * @since 1.1.0 + */ + readonly groupRefs: CreateHotToastRef[]; + + /** + * Whether group panel is expanded + * @since 1.1.0 + */ + readonly groupExpanded: boolean; + + /** + * Expand or collapse group + * @since 1.1.0 + */ + toggleGroup: (eventData?: { byAction: boolean }) => void; } -/** Event that is emitted when a snack bar is dismissed. */ +/** Event that is emitted when a toast is dismissed. */ export interface HotToastClose { - /** Whether the snack bar was dismissed using the action button. */ + /** Whether the toast was dismissed using the action button. */ dismissedByAction: boolean; id: string; } +/** Event that is emitted when a toast is expanded or collapsed. */ +export interface HotToastGroupEvent { + /** Whether the toast was expanded or collapsed using the action button. */ + byAction: boolean; + id: string; + event: 'collapse' | 'expand'; +} + export class ToastPersistConfig { /** *In which storage id vs. counts should be stored @@ -324,7 +390,7 @@ export class ToastPersistConfig { export type AddToastRef = Pick< HotToastRefProps, - 'afterClosed' | 'dispose' | 'updateMessage' | 'updateToast' | 'groupRefs' + 'afterClosed' | 'dispose' | 'updateMessage' | 'updateToast' | 'afterGroupToggled' | 'afterGroupRefsAttached' >; export type CreateHotToastRef = Omit, 'appendTo'>, 'dispose'>; diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts index 2887caf..cd2f169 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts @@ -63,13 +63,23 @@ export class HotToastService implements HotToastServiceMethods { * * @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?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultGlobalConfig.blank.content, 'blank', { - ...this._defaultGlobalConfig, - ...options, + show( + message?: Content, + options?: ToastOptions, + skipAttachToParent?: boolean + ): CreateHotToastRef { + const toast = this.createToast({ + message: message || this._defaultGlobalConfig.blank.content, + type: (options as { type: ToastType }).type ?? 'blank', + options: { + ...this._defaultGlobalConfig, + ...options, + }, + skipAttachToParent, }); return toast; @@ -84,10 +94,14 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ error(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultGlobalConfig.error.content, 'error', { - ...this._defaultGlobalConfig, - ...this._defaultGlobalConfig?.error, - ...options, + const toast = this.createToast({ + message: message || this._defaultGlobalConfig.error.content, + type: 'error', + options: { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.error, + ...options, + }, }); return toast; @@ -102,10 +116,14 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ success(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultGlobalConfig.success.content, 'success', { - ...this._defaultGlobalConfig, - ...this._defaultGlobalConfig?.success, - ...options, + const toast = this.createToast({ + message: message || this._defaultGlobalConfig.success.content, + type: 'success', + options: { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.success, + ...options, + }, }); return toast; @@ -120,10 +138,14 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ loading(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultGlobalConfig.loading.content, 'loading', { - ...this._defaultGlobalConfig, - ...this._defaultGlobalConfig?.loading, - ...options, + const toast = this.createToast({ + message: message || this._defaultGlobalConfig.loading.content, + type: 'loading', + options: { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.loading, + ...options, + }, }); return toast; @@ -138,10 +160,14 @@ export class HotToastService implements HotToastServiceMethods { * @memberof HotToastService */ warning(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultGlobalConfig.warning.content, 'warning', { - ...this._defaultGlobalConfig, - ...this._defaultGlobalConfig?.warning, - ...options, + const toast = this.createToast({ + message: message || this._defaultGlobalConfig.warning.content, + type: 'warning', + options: { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.warning, + ...options, + }, }); return toast; @@ -157,10 +183,14 @@ export class HotToastService implements HotToastServiceMethods { * @since 3.3.0 */ info(message?: Content, options?: ToastOptions): CreateHotToastRef { - const toast = this.createToast(message || this._defaultGlobalConfig.info.content, 'info', { - ...this._defaultGlobalConfig, - ...this._defaultGlobalConfig?.info, - ...options, + const toast = this.createToast({ + message: message || this._defaultGlobalConfig.info.content, + type: 'info', + options: { + ...this._defaultGlobalConfig, + ...this._defaultGlobalConfig?.info, + ...options, + }, }); return toast; @@ -275,7 +305,7 @@ export class HotToastService implements HotToastServiceMethods { }; toastRef.updateToast(updatedOptions); } else { - this.createToast(content, type, options); + this.createToast({ message: content, type, options }); } return toastRef; } catch (error) { @@ -283,12 +313,19 @@ export class HotToastService implements HotToastServiceMethods { } } - private createToast( - message: Content, - type: ToastType, - options?: DefaultToastOptions, - observableMessages?: ObservableMessages - ): CreateHotToastRef { + private createToast({ + message, + type, + options, + observableMessages, + skipAttachToParent, + }: { + message: Content; + type: ToastType; + options?: DefaultToastOptions; + observableMessages?: ObservableMessages; + skipAttachToParent?: boolean; + }): CreateHotToastRef { if (!this._isInitialized) { this._isInitialized = true; this.init(); @@ -314,7 +351,7 @@ export class HotToastService implements HotToastServiceMethods { ...options, }; - return new HotToastRef(toast).appendTo(this._componentRef.ref.instance); + return new HotToastRef(toast).appendTo(this._componentRef.ref.instance, skipAttachToParent); } } diff --git a/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss b/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss index 544e47b..679650a 100644 --- a/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss +++ b/projects/ngxpert/hot-toast/src/styles/components/_hot-toast.scss @@ -11,7 +11,10 @@ padding: var(--hot-toast-padding, 8px 10px); pointer-events: var(--hot-toast-pointer-events, auto); width: var(--hot-toast-width, fit-content); - will-change: var(--hot-toast-will-change, transform); + transition-property: border-bottom-left-radius, border-bottom-right-radius; + transition-duration: 230ms; + transition-timing-function: ease-out; + // will-change: var(--hot-toast-will-change, transform); &:hover, &:focus { @@ -21,6 +24,11 @@ @media (prefers-reduced-motion: reduce) { animation-duration: var(--hot-toast-reduced-motion-animation-duration, 10ms) !important; } + + .expanded & { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } } .hot-toast-message { @@ -90,6 +98,43 @@ } } +.hot-toast-group-btn { + align-self: var(--hot-toast-group-btn-align-self, flex-start); + background-color: var(--hot-toast-group-btn-background-color, transparent); + background-image: var( + --hot-toast-group-btn-background-image, + url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.75745 10.5858L9.17166 9.17154L12.0001 12L14.8285 9.17157L16.2427 10.5858L12.0001 14.8284L7.75745 10.5858Z' fill='currentColor' /%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21Z' fill='currentColor' /%3E%3C/svg%3E") + ); + background-position: var(--hot-toast-group-btn-background-position, center); + background-repeat: var(--hot-toast-group-btn-background-repeat, no-repeat); + background-size: var(--hot-toast-group-btn-background-size, 1.3em); + border: var(--hot-toast-group-btn-border, 0); + border-radius: var(--hot-toast-group-btn-border-radius, 0.25rem); + box-sizing: var(--hot-toast-group-btn-box-sizing, content-box); + display: var(--hot-toast-group-btn-display, flex); + height: var(--hot-toast-group-btn-height, 0.8em); + margin-top: var(--hot-toast-group-btn-margin-top, 0.25em); + opacity: var(--hot-toast-group-btn-opacity, 0.5); + padding: var(--hot-toast-group-btn-padding, 0.25em); + width: var(--hot-toast-group-btn-width, 0.8em); + will-change: var(--hot-toast-group-btn-will-change, transform); + transition: var(--hot-toast-group-btn-transition, transform 230ms cubic-bezier(0.21, 1.02, 0.73, 1)); + + &:focus { + box-shadow: var(--hot-toast-group-btn-box-shadow, 0 0 0 0.125rem rgb(13 110 253 / 25%)); + outline: var(--hot-toast-group-btn-outline, none); + } + + &:hover, + &:focus { + opacity: var(--hot-toast-group-btn-opacity, 0.75); + } + + .expanded & { + transform: rotate(var(--hot-toast-group-btn-expanded-rotate, 180deg)); + } +} + .hot-toast-icon { align-self: var(--hot-toast-icon-align-self, flex-start); padding-top: var(--hot-toast-icon-padding-top, 0.25em); @@ -100,9 +145,29 @@ } .hot-toast-bar-base-group { - background-color: var(--hot-toast-group-bg, #fff); - --hot-toast-border-radius: 0; + // remove shadow for child toasts --hot-toast-shadow: none; + + background-color: var(--hot-toast-group-bg, #fff); + margin: var(--hot-toast-margin, 16px); + margin-top: calc(-1 * var(--hot-toast-margin, 16px)); + border-bottom-left-radius: var(--hot-toast-border-radius, 4px); + border-bottom-right-radius: var(--hot-toast-border-radius, 4px); + height: 0; + overflow: hidden; + transition-property: height; + transition-duration: 230ms; + transition-timing-function: ease-in-out; + position: relative; + box-shadow: var(--hot-toast-group-after-shadow, 0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05)); + + .expanded & { + height: var(--hot-toast-group-height); + } + + .hot-toast-bar-base { + margin: 0; + } } @keyframes hotToastEnterAnimationNegative { diff --git a/src/app/app.component.html b/src/app/app.component.html index 4d83b76..3f9826d 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,117 +1,149 @@ -
-
-
- - -
- +
+
+ @for (item of jumpSections; track $index) { + +
+ {{ item.emoji }}
-
+
+
+
+ + +
+ +
+
+

The Best Angular Toast in Town

+

Smoking hot Angular notifications.

+

+ Inspired from + React Hot Toast +

+
+ + + +
+ React Hot Toast +
+ GitHub +
+ Give me a ⭐ - + +
+ Book +
+ Documentation
+
+ + + + + +
+ + + - -
- +

+ β„Ή All the code snippets are available on -
- GitHub -
- Give me a ⭐
- -
- Book -
- Documentation
-

- - + class="text-toast-600 hover:bg-gray-100 hover:text-toast-800 transform" + style="transition-property: background-color, color" + >GitHub. + - - + + -
- - - - - - - + - + - + - + -
- +
+ +
-
- + +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 342c74c..e582c95 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -11,24 +11,165 @@ import { ExampleComponent } from './sections/example/example.component'; import { StepsComponent } from './sections/steps/steps.component'; import { FeaturesComponent } from './sections/features/features.component'; import { NgClass } from '@angular/common'; +import { GroupingComponent } from './sections/grouping/grouping.component'; +import { EmojiButtonComponent } from './shared/components/emoji-button/emoji-button.component'; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'], - standalone: true, - imports: [ - NgClass, - FeaturesComponent, - StepsComponent, - ExampleComponent, - PositionComponent, - StackingComponent, - ReverseOrderComponent, - FooterComponent, - ], + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + standalone: true, + imports: [ + NgClass, + FeaturesComponent, + StepsComponent, + ExampleComponent, + PositionComponent, + StackingComponent, + GroupingComponent, + ReverseOrderComponent, + FooterComponent, + EmojiButtonComponent, + ], }) export class AppComponent { readonly repoUrl = REPO_URL; + readonly jumpSections: { href: string; emoji: string; label: string }[] = [ + { + href: '#info', + emoji: 'ℹ️', + label: 'Info', + }, + { + href: '#success', + emoji: 'βœ…', + label: 'Success', + }, + { + href: '#warning', + emoji: '⚠️', + label: 'Warning', + }, + { + href: '#error', + emoji: '❌', + label: 'Error', + }, + { + href: '#loader', + emoji: 'πŸ”„οΈ', + label: 'Loader', + }, + { + href: '#observe', + emoji: '⏳', + label: 'Observe', + }, + { + href: '#multi', + emoji: '↕️', + label: 'Multi Line', + }, + { + href: '#emoji', + emoji: 'πŸ‘', + label: 'Emoji', + }, + { + href: '#snackbar', + emoji: '🌞', + label: 'Snackbar', + }, + { + href: '#dismissible', + emoji: '❎', + label: 'dismissible', + }, + { + href: '#events', + emoji: 'πŸ”‚', + label: 'Events', + }, + { + href: '#themed', + emoji: '🎨', + label: 'Themed', + }, + { + href: '#toast-ref', + emoji: 'πŸ•΅οΈ', + label: 'Close manually', + }, + { + href: '#toast-ref-msg', + emoji: 'πŸ•΅οΈ', + label: 'Update message', + }, + { + href: '#only-one-at-a-time', + emoji: '☝️', + label: 'One at a Time', + }, + { + href: '#persistent', + emoji: 'πŸ”’', + label: 'Persistent', + }, + { + href: '#html', + emoji: 'πŸ” ', + label: 'HTML', + }, + { + href: '#template', + emoji: 'πŸ”©', + label: 'Template', + }, + { + href: '#template-data', + emoji: '🎫', + label: 'Template Data', + }, + { + href: '#component', + emoji: 'πŸ†•', + label: 'Component', + }, + { + href: '#injector', + emoji: 'πŸ’‰', + label: 'Injector', + }, + { + href: '#component-data', + emoji: 'πŸ’Ύ', + label: 'Component Data', + }, + { + href: '#positions', + emoji: 'πŸ…ΏοΈ', + label: 'Positions', + }, + { + href: '#stacking', + emoji: 'πŸͺœ', + label: 'Stacking', + }, + { + href: '#grouping-pre', + emoji: 'πŸ””', + label: 'Pre Grouped', + }, + { + href: '#grouping-post', + emoji: 'πŸ””', + label: 'Post Grouped', + }, + { + href: '#order', + emoji: 'πŸ”€', + label: 'Order', + }, + ]; constructor(private toast: HotToastService) {} @@ -54,14 +195,14 @@ export class AppComponent { } @Component({ - selector: 'app-icon', - template: 'βœ‹', - standalone: true, + selector: 'app-icon', + template: 'βœ‹', + standalone: true, }) export class IconComponent {} @Component({ - selector: 'app-msg', - template: 'Hey, how are you?', - standalone: true, + selector: 'app-msg', + template: 'Hey, how are you?', + standalone: true, }) export class MessageComponent {} diff --git a/src/app/core/services/code-highlight.service.ts b/src/app/core/services/code-highlight.service.ts index 81e1d7c..79fb52d 100644 --- a/src/app/core/services/code-highlight.service.ts +++ b/src/app/core/services/code-highlight.service.ts @@ -3,6 +3,8 @@ import { highlightElement } from 'prismjs'; import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-bash'; import 'prismjs/components/prism-typescript'; +import 'prismjs/plugins/toolbar/prism-toolbar'; +import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard'; @Injectable({ providedIn: 'root', diff --git a/src/app/sections/example/example.component.html b/src/app/sections/example/example.component.html index 5060645..77eb5e9 100644 --- a/src/app/sections/example/example.component.html +++ b/src/app/sections/example/example.component.html @@ -3,16 +3,6 @@

πŸ”— Examples

-

- β„Ή All the examples are available on - GitHub. -

@for (example of examples; track example) { diff --git a/src/app/sections/example/example.component.ts b/src/app/sections/example/example.component.ts index 758ad86..3e4cf00 100644 --- a/src/app/sections/example/example.component.ts +++ b/src/app/sections/example/example.component.ts @@ -1,6 +1,6 @@ /* eslint-disable max-len */ import { Component, Inject, Injector, OnInit, Optional, ViewChild } from '@angular/core'; -import { HotToastClose, HotToastRef, HotToastService } from '@ngxpert/hot-toast'; +import { CreateHotToastRef, HotToastClose, HotToastRef, HotToastService, ToastOptions } from '@ngxpert/hot-toast'; import { from, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { HtmlPipe } from '../../shared/pipes/html.pipe'; @@ -10,28 +10,22 @@ import { EmojiButtonComponent } from '../../shared/components/emoji-button/emoji export const EXAMPLE_EVENTS_DURATION = 5000; -interface Example { +export interface Example { id: string; title: string; subtitle?: string; action: () => void; emoji: string; - snippet: { typescript: string; html?: string }; - activeSnippet: 'typescript' | 'html'; + snippet: { typescript: string; html?: string; scss?: string; css?: string }; + activeSnippet: 'typescript' | 'html' | 'scss' | 'css'; } @Component({ - selector: 'app-example', - templateUrl: './example.component.html', - styleUrls: ['./example.component.scss'], - standalone: true, - imports: [ - EmojiButtonComponent, - NgClass, - CodeComponent, - JsonPipe, - HtmlPipe, - ], + selector: 'app-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.scss'], + standalone: true, + imports: [EmojiButtonComponent, NgClass, CodeComponent, JsonPipe, HtmlPipe, NgClass], }) export class ExampleComponent implements OnInit { @ViewChild('success') successTemplate; @@ -48,8 +42,6 @@ export class ExampleComponent implements OnInit { { label: 'HTML', value: 'html' }, ]; - readonly exampleLink = 'https://github.com/ngxpert/hot-toast/tree/master/src/app/sections/example'; - constructor(private toast: HotToastService, private parent: Injector) {} ngOnInit(): void { @@ -623,16 +615,16 @@ export class ExampleComponent implements OnInit { } @Component({ - selector: 'app-dummy', - template: 'Hi πŸ‘‹ from the component!', - standalone: true, + selector: 'app-dummy', + template: 'Hi πŸ‘‹ from the component!', + standalone: true, }) export class DummyComponent {} @Component({ - selector: 'app-injector', - template: '{{ message }}', - standalone: true, + selector: 'app-injector', + template: '{{ message }}', + standalone: true, }) export class InjectorComponent { constructor(@Optional() @Inject('MESSAGE') public message: string) {} @@ -643,9 +635,9 @@ interface DataType { } @Component({ - selector: 'app-data', - template: '{{ toastRef.data.fact }}', - standalone: true, + selector: 'app-data', + template: '{{ toastRef.data.fact }}', + standalone: true, }) export class DataComponent { constructor(@Optional() @Inject(HotToastRef) public toastRef: HotToastRef) {} diff --git a/src/app/sections/grouping/grouping.component.html b/src/app/sections/grouping/grouping.component.html new file mode 100644 index 0000000..c9322a7 --- /dev/null +++ b/src/app/sections/grouping/grouping.component.html @@ -0,0 +1,111 @@ +
+

+ πŸ”— + Grouping +

+
+ @for (example of examples; track example) { +
+ @if (example.id === "grouping-post") { @if (parentRef) { +

Parent toast is opened!

+ Add notification + } @else { + + {{ example.title }} + + } } @else { + + {{ example.title }} + + } @if (example.subtitle) { +

+ } +
+
+
+
+ +
+ +
+
+
+
+
+ } +
+
+ +
+
+ + @if (visibleToasts(toastRef.groupRefs) === 0) { πŸ”• } @else { + πŸ”” + } + +
+
+
+ @if (visibleToasts(toastRef.groupRefs) === 0) { No } @else { + {{ visibleToasts(toastRef.groupRefs) }} + } New Activities +
+
+ @if (visibleToasts(toastRef.groupRefs) === 0) { You're all caught up! } @else { What's happening around you! } +
+
+ @if (visibleToasts(toastRef.groupRefs) > 0) { + + } +
+
+ +
+
+ + {{ toastRef.data.icon }} + +
+
+ {{ toastRef.data.title }} +
{{ toastRef.data.subTitle }}
+
+
+ {{ toastRef.data.time }} +
+
+
diff --git a/src/app/sections/grouping/grouping.component.scss b/src/app/sections/grouping/grouping.component.scss new file mode 100644 index 0000000..fd3f226 --- /dev/null +++ b/src/app/sections/grouping/grouping.component.scss @@ -0,0 +1,35 @@ +@keyframes bell-keyframes { + 0% { + transform: rotate(0deg); + } + 10% { + transform: rotate(14deg); + } + 20% { + transform: rotate(-8deg); + } + 30% { + transform: rotate(14deg); + } + 40% { + transform: rotate(-4deg); + } + 50% { + transform: rotate(10deg); + } + 60% { + transform: rotate(0deg); + } + 100% { + transform: rotate(0deg); + } +} + +.bell-animation { + animation-duration: 1s; + animation-iteration-count: 1; + animation-delay: 300ms; + animation-name: bell-keyframes; + display: inline-block; + transform-origin: top center; +} diff --git a/src/app/sections/grouping/grouping.component.ts b/src/app/sections/grouping/grouping.component.ts new file mode 100644 index 0000000..a296c40 --- /dev/null +++ b/src/app/sections/grouping/grouping.component.ts @@ -0,0 +1,167 @@ +import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { Example } from '../example/example.component'; +import { CreateHotToastRef, HotToastGroupChild, HotToastService, ToastOptions } from '@ngxpert/hot-toast'; +import { Content } from '@ngneat/overview'; +import { EmojiButtonComponent } from 'src/app/shared/components/emoji-button/emoji-button.component'; +import { CodeComponent } from 'src/app/shared/components/code/code.component'; +import { HtmlPipe } from 'src/app/shared/pipes/html.pipe'; +import { NgClass } from '@angular/common'; +import { preGroupingTS, preGroupingHTML, preGroupingCSS, postGroupingTS } from './snippets'; + +@Component({ + selector: 'app-grouping', + templateUrl: 'grouping.component.html', + standalone: true, + imports: [EmojiButtonComponent, CodeComponent, HtmlPipe, NgClass], + styleUrls: ['./grouping.component.scss'], +}) +export class GroupingComponent implements OnInit { + toast = inject(HotToastService); + examples: Example[] = []; + @ViewChild('groupTemplate') ngTemplateGroup; + @ViewChild('groupItemTemplate') ngTemplateGroupItem; + + parentRef: CreateHotToastRef; + + private notificationCounter = 0; + + readonly snippetLanguages: { label: string; value: 'typescript' | 'html' | 'css' }[] = [ + { label: 'TypeScript', value: 'typescript' }, + { label: 'HTML', value: 'html' }, + { label: 'CSS', value: 'css' }, + ]; + readonly commonOptions: ToastOptions = { autoClose: false }; + readonly childNotifications = (ngTemplateGroupItem: Content): HotToastGroupChild[] => [ + { + options: { + message: ngTemplateGroupItem, + data: { + title: 'New Message!', + subTitle: 'Sarah sent you a message.', + time: 'Just Now', + icon: 'πŸ—¨οΈ', + }, + ...this.commonOptions, + }, + }, + { + options: { + message: ngTemplateGroupItem, + data: { + title: 'Level Up!', + subTitle: "You've unlocked a new achievement.", + time: '2 min ago', + icon: 'βœ…', + }, + ...this.commonOptions, + }, + }, + { + options: { + message: ngTemplateGroupItem, + data: { + title: 'Reminder: Meeting Today', + subTitle: 'Your team meeting starts in 30 minutes.', + time: '1 hours ago', + icon: '⏰', + }, + ...this.commonOptions, + }, + }, + { + options: { + message: ngTemplateGroupItem, + data: { + title: 'Special Offer!', + subTitle: 'Save 20% off on subscription upgrade.', + time: '12 hours ago', + icon: '🏷️', + }, + ...this.commonOptions, + }, + }, + { + options: { + message: ngTemplateGroupItem, + data: { + title: 'Task Assigned', + subTitle: 'A new task is awaiting your action.', + time: 'Yesterday', + icon: 'βœ”οΈ', + }, + ...this.commonOptions, + }, + }, + ]; + + ngOnInit(): void { + const examples: Example[] = [ + { + id: 'grouping-pre', + title: 'Show Pre-Grouped Notifications', + subtitle: `

If you need to group toasts, you can use group.children option. This is useful if you want to show for example notifications as grouped items.

+

+ πŸ‘‰ You can access children group toast references using toastRef.groupsRefs +

+ `, + emoji: 'πŸ””', + activeSnippet: 'typescript', + snippet: { + typescript: preGroupingTS, + html: preGroupingHTML, + css: preGroupingCSS, + }, + action: () => { + this.toast.show(this.ngTemplateGroup, { + position: 'top-right', + autoClose: false, + className: 'hot-toast-custom-class', + group: { + className: 'hot-toast-custom-class', + children: this.childNotifications(this.ngTemplateGroupItem), + }, + }); + }, + }, + { + id: 'grouping-post', + title: 'Open the first toast', + subtitle: + 'If you need to add children in existing toast, you can use group.parent option. This is useful if you want to show for example dynamic notifications in an already opened toast.', + emoji: 'πŸ””', + activeSnippet: 'typescript', + snippet: { + typescript: postGroupingTS, + html: preGroupingHTML, + css: preGroupingCSS, + }, + action: () => { + this.parentRef = this.toast.show(this.ngTemplateGroup, { + position: 'top-right', + autoClose: false, + className: 'hot-toast-custom-class', + group: { + className: 'hot-toast-custom-class', + }, + }); + }, + }, + ]; + + Array.prototype.push.apply(this.examples, examples); + } + + visibleToasts(toastRefs: CreateHotToastRef[]) { + return toastRefs.filter((t) => t.getToast().visible).length; + } + + addNotification() { + const allNotifications = this.childNotifications(this.ngTemplateGroupItem); + const toast = allNotifications[this.notificationCounter++ % allNotifications.length].options; + this.toast.show(toast.message, { ...toast, group: { parent: this.parentRef } }); + } + + click(e: Example) { + e.action(); + } +} diff --git a/src/app/sections/grouping/snippets.ts b/src/app/sections/grouping/snippets.ts new file mode 100644 index 0000000..ccc5edb --- /dev/null +++ b/src/app/sections/grouping/snippets.ts @@ -0,0 +1,228 @@ +const childNotifications = ` +readonly commonOptions: ToastOptions<unknown> = { autoClose: false }; + +readonly childNotifications = (ngTemplateGroupItem: Content): HotToastGroupChild[] => [ +{ + options: { + message: ngTemplateGroupItem, + data: { + title: 'New Message!', + subTitle: 'Sarah sent you a message.', + time: 'Just Now', + icon: 'πŸ—¨οΈ', + }, + ...this.commonOptions, + }, +}, +{ + options: { + message: ngTemplateGroupItem, + data: { + title: 'Level Up!', + subTitle: "You've unlocked a new achievement.", + time: '2 min ago', + icon: 'βœ…', + }, + ...this.commonOptions, + }, +}, +{ + options: { + message: ngTemplateGroupItem, + data: { + title: 'Reminder: Meeting Today', + subTitle: 'Your team meeting starts in 30 minutes.', + time: '1 hours ago', + icon: '⏰', + }, + ...this.commonOptions, + }, +}, +{ + options: { + message: ngTemplateGroupItem, + data: { + title: 'Special Offer!', + subTitle: 'Save 20% off on subscription upgrade.', + time: '12 hours ago', + icon: '🏷️', + }, + ...this.commonOptions, + }, +}, +{ + options: { + message: ngTemplateGroupItem, + data: { + title: 'Task Assigned', + subTitle: 'A new task is awaiting your action.', + time: 'Yesterday', + icon: 'βœ”οΈ', + }, + ...this.commonOptions, + }, +}, +];`; + +export const preGroupingTS = ` +@ViewChild('groupTemplate') ngTemplateGroup; +@ViewChild('groupItemTemplate') ngTemplateGroupItem; + +toast = inject(HotToastService); + +showPreGroupedNotifications() { + this.toast.show(ngTemplateGroup, { + position: 'top-right', + autoClose: false, + className: 'hot-toast-custom-class', + group: { + className: 'hot-toast-custom-class', + children: this.childNotifications(this.ngTemplateGroupItem), + } + }) +} + +visibleToasts(toastRefs: CreateHotToastRef<unknown>[]) { + return toastRefs.filter((t) => t.getToast().visible).length; +} + +${childNotifications} +`; + +export const preGroupingHTML = ` +<ng-template #groupTemplate let-toastRef> + <div class="flex gap-x-2 w-full h-[56px] items-center"> + <div + class="bg-slate-100 rounded transition-all ease-in-out duration-[230ms] flex items-center justify-center text-xl" + [ngClass]="{ 'w-10 h-10': toastRef.groupExpanded, 'w-14 h-14': !toastRef.groupExpanded }" + > + <span + class="transition-all ease-in-out duration-[230ms] drop-shadow-md" + [ngClass]="{ 'scale-1': toastRef.groupExpanded, 'scale-125': !toastRef.groupExpanded }" + > + @if (visibleToasts(toastRef.groupRefs) === 0) { πŸ”• } @else { + <span class="bell-animation"> πŸ”” </span> + } + </span> + </div> + <div> + <div + class="font-medium transition-all ease-in-out duration-[230ms]" + [ngClass]="{ + 'scale-125': !toastRef.groupExpanded, + 'scale-1 pl-0': toastRef.groupExpanded, + 'pl-[22px]': !toastRef.groupExpanded && visibleToasts(toastRef.groupRefs) !== 0, + 'pl-[14px]': !toastRef.groupExpanded && visibleToasts(toastRef.groupRefs) === 0 + }" + > + @if (visibleToasts(toastRef.groupRefs) === 0) { No } @else { + {{ visibleToasts(toastRef.groupRefs) }} + } New Activities + </div> + <div + class="text-gray-500 transition-all ease-in-out duration-[230ms]" + [ngClass]="{ 'scale-90 ml-[-10px]': toastRef.groupExpanded, 'ml-0': !toastRef.groupExpanded }" + > + @if (visibleToasts(toastRef.groupRefs) === 0) { You're all caught up! } @else { What's happening around you! } + </div> + </div> + <button + (click)="toastRef.toggleGroup()" + class="ml-auto self-center hot-toast-group-btn" + [class.expanded]="toastRef.groupExpanded" + [attr.aria-label]="toastRef.groupExpanded ? 'Collapse' : 'Expand'" + ></button> + </div> +</ng-template> +<ng-template #groupItemTemplate let-toastRef> + <div class="flex gap-x-2 w-full"> + <div class="bg-slate-100 rounded w-10 h-10 flex items-center justify-center text-xl"> + <span class="drop-shadow-md"> + {{ toastRef.data.icon }} + </span> + </div> + <div> + {{ toastRef.data.title }} + <div class="text-sm text-gray-500">{{ toastRef.data.subTitle }}</div> + </div> + <div class="text-xs text-gray-500 ml-auto"> + {{ toastRef.data.time }} + </div> + </div> +</ng-template>`; + +export const preGroupingCSS = ` +.hot-toast-custom-class { + --hot-toast-width: 390px; + --hot-toast-max-width: var(--hot-toast-width); + --hot-toast-message-justify-content: start; + --hot-toast-message-margin: 4px 0; + --hot-toast-group-btn-align-self: center; +} + + +@keyframes bell-keyframes { + 0% { + transform: rotate(0deg); + } + 10% { + transform: rotate(14deg); + } + 20% { + transform: rotate(-8deg); + } + 30% { + transform: rotate(14deg); + } + 40% { + transform: rotate(-4deg); + } + 50% { + transform: rotate(10deg); + } + 60% { + transform: rotate(0deg); + } + 100% { + transform: rotate(0deg); + } +} + +.bell-animation { + animation-duration: 1s; + animation-iteration-count: var(--ring-bell); + animation-name: bell-keyframes; + display: inline-block; + transform-origin: top center; +}`; + +export const postGroupingTS = ` +@ViewChild('groupTemplate') ngTemplateGroup; +@ViewChild('groupItemTemplate') ngTemplateGroupItem; + +toast = inject(HotToastService); +parentRef: CreateHotToastRef<unknown>; + +showFirstToast() { + this.parentRef = this.toast.show(ngTemplateGroup, { + position: 'top-right', + autoClose: false, + className: 'hot-toast-custom-class', + group: { + className: 'hot-toast-custom-class' + } + }) +} + +visibleToasts(toastRefs: CreateHotToastRef<unknown>[]) { + return toastRefs.filter((t) => t.getToast().visible).length; +} + +addNotification() { + const allNotifications = this.childNotifications(this.ngTemplateGroupItem); + const toast = allNotifications[this.notificationCounter++ % allNotifications.length].options; + this.toast.show(toast.message, { ...toast, group: { parent: this.parentRef } }); +} + +${childNotifications} +`; diff --git a/src/app/sections/position/position.component.html b/src/app/sections/position/position.component.html index 8afeae0..9e36d01 100644 --- a/src/app/sections/position/position.component.html +++ b/src/app/sections/position/position.component.html @@ -32,7 +32,7 @@

- +

@@ -56,7 +56,7 @@

- +

diff --git a/src/app/sections/reverse-order/reverse-order.component.html b/src/app/sections/reverse-order/reverse-order.component.html index ff89d57..4469a13 100644 --- a/src/app/sections/reverse-order/reverse-order.component.html +++ b/src/app/sections/reverse-order/reverse-order.component.html @@ -32,7 +32,7 @@

- +

@@ -54,7 +54,7 @@

- +

diff --git a/src/app/sections/stacking/stacking.component.html b/src/app/sections/stacking/stacking.component.html index d8eab84..4a905b5 100644 --- a/src/app/sections/stacking/stacking.component.html +++ b/src/app/sections/stacking/stacking.component.html @@ -38,7 +38,7 @@

- +
@@ -60,7 +60,7 @@

- +
diff --git a/src/app/shared/components/code/code.component.html b/src/app/shared/components/code/code.component.html index ff5df29..22b4c33 100644 --- a/src/app/shared/components/code/code.component.html +++ b/src/app/shared/components/code/code.component.html @@ -1,5 +1,5 @@
-
+  
      
   
diff --git a/src/app/shared/components/code/code.component.scss b/src/app/shared/components/code/code.component.scss index e69de29..e5e4edc 100644 --- a/src/app/shared/components/code/code.component.scss +++ b/src/app/shared/components/code/code.component.scss @@ -0,0 +1,12 @@ +:host > div { + position: relative; + overflow-x: hidden; + &:hover { + --clipboard-right: 16px; + } +} + +pre { + max-height: 300px; + overflow-y: auto; +} diff --git a/src/app/shared/components/emoji-button/emoji-button.component.html b/src/app/shared/components/emoji-button/emoji-button.component.html index d669f19..a446c84 100644 --- a/src/app/shared/components/emoji-button/emoji-button.component.html +++ b/src/app/shared/components/emoji-button/emoji-button.component.html @@ -11,6 +11,7 @@ }}" (click)="btnClick.emit()" [id]="btnId" + [disabled]="disabled" > {{ emoji }} diff --git a/src/app/shared/components/emoji-button/emoji-button.component.ts b/src/app/shared/components/emoji-button/emoji-button.component.ts index 2bf04c1..3a48320 100644 --- a/src/app/shared/components/emoji-button/emoji-button.component.ts +++ b/src/app/shared/components/emoji-button/emoji-button.component.ts @@ -12,4 +12,5 @@ export class EmojiButtonComponent { @Input() btnId: string; @Output() btnClick = new EventEmitter(); @Input() showLink = false; + @Input() disabled = false; } diff --git a/src/styles.scss b/src/styles.scss index 9a4aa19..943903f 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,3 +1,13 @@ @use './styles/tailwind-utils.css'; @use './styles/main.css'; @use '../projects/ngxpert/hot-toast/src/styles/styles.scss'; +@use "./styles/grouping.css"; + +.copy-to-clipboard-button { + position: absolute; + color: white; + top: 8px; + right: var(--clipboard-right, -28px); + font-size: larger; + transition: right 230ms ease-in-out; +} \ No newline at end of file diff --git a/src/styles/grouping.css b/src/styles/grouping.css new file mode 100644 index 0000000..df96d46 --- /dev/null +++ b/src/styles/grouping.css @@ -0,0 +1,7 @@ +.hot-toast-custom-class { + --hot-toast-width: 390px; + --hot-toast-max-width: var(--hot-toast-width); + --hot-toast-message-justify-content: start; + --hot-toast-message-margin: 4px 0; + --hot-toast-group-btn-align-self: center; +} From 4166acf10316e5abf280f5c52c9e5f92b6e6896c Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Wed, 17 Apr 2024 00:21:51 +0530 Subject: [PATCH 3/9] docs: update content for model --- README.md | 25 ++++++++++--------- .../hot-toast/src/lib/hot-toast.model.ts | 5 +--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f62d60d..f4317cc 100644 --- a/README.md +++ b/README.md @@ -262,25 +262,26 @@ All options, which are set _Available in global config?_ from `ToastOptions` are Configuration used when opening an hot-toast. -| Name | Type | Description | Available in global config? | -| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | -| id | `string` | Unique id to associate with hot-toast. There can't be multiple hot-toasts opened with same id.
_[Example](https://ngxpert.github.io/hot-toast/#only-one-at-a-time)_ | No | -| duration | `number` | Duration in milliseconds after which hot-toast will be auto closed. Can be disabled via `autoClose: false`
_Default: `3000, error = 4000, loading = 30000`_ | Yes | -| autoClose | `boolean` | Auto close hot-toast after duration
_Default: `true`_ | Yes | +| Name | Type | Description | Available in global config? | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| id | `string` | Unique id to associate with hot-toast. There can't be multiple hot-toasts opened with same id.
_[Example](https://ngxpert.github.io/hot-toast/#only-one-at-a-time)_ | No | +| duration | `number` | Duration in milliseconds after which hot-toast will be auto closed. Can be disabled via `autoClose: false`
_Default: `3000, error = 4000, loading = 30000`_ | Yes | +| autoClose | `boolean` | Auto close hot-toast after duration
_Default: `true`_ | Yes | | position | [`ToastPosition`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=export%20type%20ToastPosition) | The position to place the hot-toast.
_Default: `top-center`_
_[Example](https://ngxpert.github.io/hot-toast/#positions)_ | Yes | -| dismissible | `boolean` | Show close button in hot-toast
_Default: `false`_
_[Example](https://ngxpert.github.io/hot-toast/#dismissible)_ | Yes | +| dismissible | `boolean` | Show close button in hot-toast
_Default: `false`_
_[Example](https://ngxpert.github.io/hot-toast/#dismissible)_ | Yes | | role | [`ToastRole`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=export%20type%20ToastRole) | Role of the live region.
_Default: `status`_ | Yes | | ariaLive | [`ToastAriaLive`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=export%20type%20ToastAriaLive) | aria-live value for the live region.
_Default: `polite`_ | Yes | | theme | [`ToastTheme`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=export%20type%20ToastTheme) | Visual appearance of hot-toast
_Default: `toast`_
_[Example](https://ngxpert.github.io/hot-toast/#snackbar)_ | Yes | | persist | [`{ToastPersistConfig}`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=export%20class%20ToastPersistConfig) | Useful when you want to keep a persistance for toast based on ids, across sessions.
_[Example](https://ngxpert.github.io/hot-toast/#persistent)_ | No | -| icon | [`Content`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/overview/blob/main/projects/ngxpert/overview/src/lib/views/types.ts&q=export%20type%20Content) | Icon to show in the hot-toast
_[Example](https://ngxpert.github.io/hot-toast/#emoji)_ | Yes | +| icon | [`Content`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/overview/blob/main/projects/ngxpert/overview/src/lib/views/types.ts&q=export%20type%20Content) | Icon to show in the hot-toast
_[Example](https://ngxpert.github.io/hot-toast/#emoji)_ | Yes | | iconTheme | [`IconTheme`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=export%20type%20IconTheme) | Use this to change icon color
_[Example](https://ngxpert.github.io/hot-toast/#themed)_ | Yes | -| className | `string` | Extra CSS classes to be added to the hot toast container. | Yes | -| attributes | [`Record`](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeystype) | Extra attributes to be added to the hot toast container. Can be used for e2e tests. | Yes | -| style | `style object` | Extra styles to apply for hot-toast.
_[Example](https://ngxpert.github.io/hot-toast/#themed)_ | Yes | -| closeStyle | `style object` | Extra styles to apply for close button | Yes | +| className | `string` | Extra CSS classes to be added to the hot toast container. | Yes | +| attributes | [`Record`](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeystype) | Extra attributes to be added to the hot toast container. Can be used for e2e tests. | Yes | +| style | `style object` | Extra styles to apply for hot-toast.
_[Example](https://ngxpert.github.io/hot-toast/#themed)_ | Yes | +| closeStyle | `style object` | Extra styles to apply for close button | Yes | | data | [`DataType`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=export%20interface%20Toast%3CDataType%3E) | Allows you to pass data for your template and component. You can access the data using `toastRef.data`.
_Examples: [Template with Data](https://ngxpert.github.io/hot-toast/#template-data), [Component with Data](https://ngxpert.github.io/hot-toast/#component-data)_ | No | -| injector | `Injector` | Allows you to pass injector for your component.
_[Example](https://ngxpert.github.io/hot-toast/#injector)_ | No | +| injector | `Injector` | Allows you to pass injector for your component.
_[Example](https://ngxpert.github.io/hot-toast/#injector)_ | No | +| group | [`group`](https://github-link.vercel.app/api?ghUrl=https://github.com/ngxpert/hot-toast/blob/main/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts&q=group%3F) | Allows you to set group options.
Examples: [Pre-Grouping](https://ngxpert.github.io/hot-toast/#pre-grouping), [Post-Grouping](https://ngxpert.github.io/hot-toast/#post-grouping) | No | --- diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts index 8d7d6b1..9582f6e 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast.model.ts @@ -204,13 +204,13 @@ export interface Toast { /** * Allows you to pass data for your component/template * - * @since 2.0.0 * @type {DataType} * @memberof Toast */ data?: DataType; /** + * Allows you to set group options * @since 1.1.0 */ group?: { @@ -319,9 +319,6 @@ export interface HotToastRefProps { /**Closes the toast */ close: (closeData?: { dismissedByAction: boolean }) => void; - /** - * @since 2.0.0 - */ data: DataType; /** From 8ecbbc2a3c10b6e3006debd5aad7e304c7984d71 Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Wed, 17 Apr 2024 00:25:44 +0530 Subject: [PATCH 4/9] fix: make options optional in show function --- projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts b/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts index cd2f169..53bbe12 100644 --- a/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts +++ b/projects/ngxpert/hot-toast/src/lib/hot-toast.service.ts @@ -74,7 +74,7 @@ export class HotToastService implements HotToastServiceMethods { ): CreateHotToastRef { const toast = this.createToast({ message: message || this._defaultGlobalConfig.blank.content, - type: (options as { type: ToastType }).type ?? 'blank', + type: (options as { type: ToastType })?.type ?? 'blank', options: { ...this._defaultGlobalConfig, ...options, From 58a188a2b976fc43295f92858897700b0c9836d5 Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Wed, 17 Apr 2024 00:47:18 +0530 Subject: [PATCH 5/9] fix: handle expanded in doCheck --- .../hot-toast-container.component.ts | 6 ++++-- .../components/hot-toast/hot-toast.component.ts | 16 ++++++++++++---- src/app/sections/example/example.component.html | 12 ++++++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts index fa95dff..ce6fb71 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts @@ -54,11 +54,13 @@ export class HotToastContainerComponent { } getVisibleToasts(position: ToastPosition) { - return this.toasts.filter((t) => t.group?.parent === undefined && t.visible && t.position === position); + return this.unGroupedToasts.filter((t) => t.visible && t.position === position); } get unGroupedToasts() { - return this.toasts.filter((t) => t.group?.parent === undefined); + return this.toasts.filter( + (t) => t.group?.parent === undefined || t.group?.children === undefined || t.group?.children.length === 0 + ); } calculateOffset(toastId: string, position: ToastPosition) { diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts index 253a05d..c959ca3 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.ts @@ -76,6 +76,7 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh isManualClose = false; context: Record; toastComponentInjector: Injector; + isExpanded = false; private unlisteners: VoidFunction[] = []; private softClosed = false; @@ -173,10 +174,6 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh .reduce((prev, curr) => prev + curr, 0); } - get isExpanded() { - return this.toastRef.groupExpanded; - } - get visibleToasts() { return this.groupChildrenToasts.filter((t) => t.visible); } @@ -186,6 +183,12 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh this.groupRefs = this.toastRef.groupRefs.slice(); this.cdr.detectChanges(); + this.emiHeightWithGroup(this.isExpanded); + } + if (this.toastRef.groupExpanded !== this.isExpanded) { + this.isExpanded = this.toastRef.groupExpanded; + this.cdr.detectChanges(); + this.emiHeightWithGroup(this.isExpanded); } } @@ -253,7 +256,12 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh animate(nativeElement, exitAnimation); this.softClosed = true; + + if (this.isExpanded) { + this.toggleToastGroup(); + } } + softOpen() { const softEnterAnimation = `hotToastEnterSoftAnimation${ top ? 'Negative' : 'Positive' diff --git a/src/app/sections/example/example.component.html b/src/app/sections/example/example.component.html index 77eb5e9..3a4a7a1 100644 --- a/src/app/sections/example/example.component.html +++ b/src/app/sections/example/example.component.html @@ -43,11 +43,15 @@

Settings saved! Could not save. - Custom and bold  - +
+ Custom and bold  + +
- Custom and bold with data: {{ toastRef?.data | json }}
+
+ Custom and bold with data: {{ toastRef?.data | json }}
+
From 6882db8b0ca31e354361b63e76146a31a527f7af Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Wed, 17 Apr 2024 11:33:37 +0530 Subject: [PATCH 6/9] docs: install cmdk --- package-lock.json | 100 ++++++- package.json | 4 +- src/app/app.component.html | 261 +++++++++--------- src/app/app.component.ts | 187 ++++--------- .../sections/grouping/grouping.component.html | 2 +- .../jump-to-dialog.component.html | 17 ++ .../jump-to-dialog.component.ts | 214 ++++++++++++++ src/index.html | 1 + src/styles.scss | 2 + 9 files changed, 518 insertions(+), 270 deletions(-) create mode 100644 src/app/shared/components/jump-to-dialog/jump-to-dialog.component.html create mode 100644 src/app/shared/components/jump-to-dialog/jump-to-dialog.component.ts diff --git a/package-lock.json b/package-lock.json index 94e79ef..7f8bddb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0-development", "dependencies": { "@angular/animations": "^17.1.3", + "@angular/cdk": "~17.3.4", "@angular/common": "^17.1.3", "@angular/compiler": "^17.1.3", "@angular/core": "^17.1.3", @@ -17,6 +18,7 @@ "@angular/platform-browser-dynamic": "^17.1.3", "@angular/router": "^17.1.3", "@ngneat/overview": "6.0.0", + "@ngxpert/cmdk": "^1.0.0", "prismjs": "^1.23.0", "rxjs": "~6.6.0", "tslib": "^2.3.1", @@ -1473,6 +1475,34 @@ "@angular/core": "17.1.3" } }, + "node_modules/@angular/cdk": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.4.tgz", + "integrity": "sha512-/wbKUbc0YC3HGE2TCgW7D07Q99PZ/5uoRvMyWw0/wHa8VLNavXZPecbvtyLs//3HnqoCMSUFE7E2Mrd7jAWfcA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cdk/node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "optional": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@angular/cli": { "version": "17.1.3", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.1.3.tgz", @@ -5176,6 +5206,18 @@ "integrity": "sha512-o9quceIAomYD+P/40tJ1n1G/CkWcAa1dWW5/acCz4qnS7ajhGsBoSD4N+usi+vqdoEJnunCsoEBWL9OMsSwcFA==", "dev": true }, + "node_modules/@ngneat/until-destroy": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/until-destroy/-/until-destroy-10.0.0.tgz", + "integrity": "sha512-xXFAabQ4YVJ82LYxdgUlaKZyR3dSbxqG3woSyaclzxfCgWMEDweCcM/GGYbNiHJa0WwklI98RXHvca+UyCxpeg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=13", + "rxjs": "^6.4.0 || ^7.0.0" + } + }, "node_modules/@ngtools/webpack": { "version": "17.1.3", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.1.3.tgz", @@ -5192,6 +5234,21 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngxpert/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ngxpert/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-PmZEKINJxWbGEE4GJ+iyB881z2YWETL1s8l2snqnmmVKCzTZjQruCY0JA8rLIAmLz6m9jlXUVj3Ox05i216tCQ==", + "dependencies": { + "@angular/cdk": ">=16.0.0", + "@ngneat/overview": ">=5.0.0", + "@ngneat/until-destroy": ">=10.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">16.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12838,7 +12895,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.12" }, @@ -30637,6 +30694,26 @@ "tslib": "^2.3.0" } }, + "@angular/cdk": { + "version": "17.3.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.4.tgz", + "integrity": "sha512-/wbKUbc0YC3HGE2TCgW7D07Q99PZ/5uoRvMyWw0/wHa8VLNavXZPecbvtyLs//3HnqoCMSUFE7E2Mrd7jAWfcA==", + "requires": { + "parse5": "^7.1.2", + "tslib": "^2.3.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "optional": true, + "requires": { + "entities": "^4.4.0" + } + } + } + }, "@angular/cli": { "version": "17.1.3", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.1.3.tgz", @@ -33156,6 +33233,14 @@ "integrity": "sha512-o9quceIAomYD+P/40tJ1n1G/CkWcAa1dWW5/acCz4qnS7ajhGsBoSD4N+usi+vqdoEJnunCsoEBWL9OMsSwcFA==", "dev": true }, + "@ngneat/until-destroy": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/until-destroy/-/until-destroy-10.0.0.tgz", + "integrity": "sha512-xXFAabQ4YVJ82LYxdgUlaKZyR3dSbxqG3woSyaclzxfCgWMEDweCcM/GGYbNiHJa0WwklI98RXHvca+UyCxpeg==", + "requires": { + "tslib": "^2.3.0" + } + }, "@ngtools/webpack": { "version": "17.1.3", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.1.3.tgz", @@ -33163,6 +33248,17 @@ "dev": true, "requires": {} }, + "@ngxpert/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ngxpert/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-PmZEKINJxWbGEE4GJ+iyB881z2YWETL1s8l2snqnmmVKCzTZjQruCY0JA8rLIAmLz6m9jlXUVj3Ox05i216tCQ==", + "requires": { + "@angular/cdk": ">=16.0.0", + "@ngneat/overview": ">=5.0.0", + "@ngneat/until-destroy": ">=10.0.0", + "tslib": "^2.3.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -38800,7 +38896,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true + "devOptional": true }, "env-ci": { "version": "11.0.0", diff --git a/package.json b/package.json index beaabcd..7260dc6 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@angular/animations": "^17.1.3", + "@angular/cdk": "~17.3.4", "@angular/common": "^17.1.3", "@angular/compiler": "^17.1.3", "@angular/core": "^17.1.3", @@ -46,6 +47,7 @@ "@angular/platform-browser-dynamic": "^17.1.3", "@angular/router": "^17.1.3", "@ngneat/overview": "6.0.0", + "@ngxpert/cmdk": "^1.0.0", "prismjs": "^1.23.0", "rxjs": "~6.6.0", "tslib": "^2.3.1", @@ -116,4 +118,4 @@ "path": "./node_modules/cz-conventional-changelog" } } -} +} \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index 3f9826d..fc81ec1 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,149 +1,156 @@ -
-
- @for (item of jumpSections; track $index) { - -
- {{ item.emoji }} + - -
- -
-
- + +--> - -
- +
+
+
+ + +
+ +
+
+

The Best Angular Toast in Town

+

Smoking hot Angular notifications.

+

+ Inspired from -
- GitHub -
- Give me a ⭐
- React Hot Toast -
- Book -
- Documentation -

- - - - - - +

- - - -

- β„Ή All the code snippets are available on + +
+ GitHub. -

+ rel="noopener noreferrer" + [ngClass]="[ + 'rounded-lg flex font-bold bg-white py-4 px-6 shadow-button text-toast-800', + 'active:translate-y-0.5 active:shadow-button-active transform' + ]" + style="transition-property: box-shadow, transform" + > +
+ GitHub +
+ Give me a ⭐ + +
+ Book +
+ Documentation
+ + + - - + + + - + + + - +

+ β„Ή All the code snippets are available on + GitHub. +

- + + + - + -
- -
- + - + + + + +
+ +
+ + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e582c95..e1426fb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,6 +2,9 @@ import { Component } from '@angular/core'; import { HotToastService } from '@ngxpert/hot-toast'; import { from, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; +import { NgClass } from '@angular/common'; +// import { Dialog } from '@angular/cdk/dialog'; + import { REPO_URL } from './core/constants'; import { FooterComponent } from './sections/footer/footer.component'; import { ReverseOrderComponent } from './sections/reverse-order/reverse-order.component'; @@ -10,9 +13,9 @@ import { PositionComponent } from './sections/position/position.component'; import { ExampleComponent } from './sections/example/example.component'; import { StepsComponent } from './sections/steps/steps.component'; import { FeaturesComponent } from './sections/features/features.component'; -import { NgClass } from '@angular/common'; import { GroupingComponent } from './sections/grouping/grouping.component'; import { EmojiButtonComponent } from './shared/components/emoji-button/emoji-button.component'; +// import { JumpToDialogComponent } from './shared/components/jump-to-dialog/jump-to-dialog.component'; @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -29,149 +32,55 @@ import { EmojiButtonComponent } from './shared/components/emoji-button/emoji-but ReverseOrderComponent, FooterComponent, EmojiButtonComponent, + // JumpToDialogComponent, ], }) export class AppComponent { readonly repoUrl = REPO_URL; - readonly jumpSections: { href: string; emoji: string; label: string }[] = [ - { - href: '#info', - emoji: 'ℹ️', - label: 'Info', - }, - { - href: '#success', - emoji: 'βœ…', - label: 'Success', - }, - { - href: '#warning', - emoji: '⚠️', - label: 'Warning', - }, - { - href: '#error', - emoji: '❌', - label: 'Error', - }, - { - href: '#loader', - emoji: 'πŸ”„οΈ', - label: 'Loader', - }, - { - href: '#observe', - emoji: '⏳', - label: 'Observe', - }, - { - href: '#multi', - emoji: '↕️', - label: 'Multi Line', - }, - { - href: '#emoji', - emoji: 'πŸ‘', - label: 'Emoji', - }, - { - href: '#snackbar', - emoji: '🌞', - label: 'Snackbar', - }, - { - href: '#dismissible', - emoji: '❎', - label: 'dismissible', - }, - { - href: '#events', - emoji: 'πŸ”‚', - label: 'Events', - }, - { - href: '#themed', - emoji: '🎨', - label: 'Themed', - }, - { - href: '#toast-ref', - emoji: 'πŸ•΅οΈ', - label: 'Close manually', - }, - { - href: '#toast-ref-msg', - emoji: 'πŸ•΅οΈ', - label: 'Update message', - }, - { - href: '#only-one-at-a-time', - emoji: '☝️', - label: 'One at a Time', - }, - { - href: '#persistent', - emoji: 'πŸ”’', - label: 'Persistent', - }, - { - href: '#html', - emoji: 'πŸ” ', - label: 'HTML', - }, - { - href: '#template', - emoji: 'πŸ”©', - label: 'Template', - }, - { - href: '#template-data', - emoji: '🎫', - label: 'Template Data', - }, - { - href: '#component', - emoji: 'πŸ†•', - label: 'Component', - }, - { - href: '#injector', - emoji: 'πŸ’‰', - label: 'Injector', - }, - { - href: '#component-data', - emoji: 'πŸ’Ύ', - label: 'Component Data', - }, - { - href: '#positions', - emoji: 'πŸ…ΏοΈ', - label: 'Positions', - }, - { - href: '#stacking', - emoji: 'πŸͺœ', - label: 'Stacking', - }, - { - href: '#grouping-pre', - emoji: 'πŸ””', - label: 'Pre Grouped', - }, - { - href: '#grouping-post', - emoji: 'πŸ””', - label: 'Post Grouped', - }, - { - href: '#order', - emoji: 'πŸ”€', - label: 'Order', - }, - ]; + isDialogOpen = false; + + constructor(private toast: HotToastService) // private dialog: Dialog + {} + + // keyDownListener(ev: KeyboardEvent) { + // const key = ev.key; + // const element = document.querySelector('[data-keyboard-key="' + key.toUpperCase() + '"]'); + // element.classList.add('active'); + // } + + // keyUpListener(ev: KeyboardEvent) { + // const key = ev.key; + // const element = document.querySelector('[data-keyboard-key="' + key.toUpperCase() + '"]'); + // element.classList.remove('active'); + // if (key === '/') { + // ev.preventDefault(); + // this.openDialog(); + // } + // } + + // ngOnInit() { + // document.addEventListener('keyup', this.keyUpListener); + // document.addEventListener('keydown', this.keyDownListener); + // } + + // ngOnDestroy() { + // document.removeEventListener('keyup', this.keyUpListener); + // document.removeEventListener('keydown', this.keyDownListener); + // } + + // openDialog() { + // if (!this.isDialogOpen) { + // this.isDialogOpen = true; + + // const dialogRef = this.dialog.open(JumpToDialogComponent, { + // width: '350px', + // }); - constructor(private toast: HotToastService) {} + // dialogRef.closed.subscribe((result) => { + // this.isDialogOpen = false; + // }); + // } + // } observe() { const promise = new Promise((res, rej) => { diff --git a/src/app/sections/grouping/grouping.component.html b/src/app/sections/grouping/grouping.component.html index c9322a7..2dc90ae 100644 --- a/src/app/sections/grouping/grouping.component.html +++ b/src/app/sections/grouping/grouping.component.html @@ -1,4 +1,4 @@ -
+

πŸ”— Grouping diff --git a/src/app/shared/components/jump-to-dialog/jump-to-dialog.component.html b/src/app/shared/components/jump-to-dialog/jump-to-dialog.component.html new file mode 100644 index 0000000..895fc70 --- /dev/null +++ b/src/app/shared/components/jump-to-dialog/jump-to-dialog.component.html @@ -0,0 +1,17 @@ +
+ + + +
No results found.
+ + + + + + +
+
+
diff --git a/src/app/shared/components/jump-to-dialog/jump-to-dialog.component.ts b/src/app/shared/components/jump-to-dialog/jump-to-dialog.component.ts new file mode 100644 index 0000000..79efe0f --- /dev/null +++ b/src/app/shared/components/jump-to-dialog/jump-to-dialog.component.ts @@ -0,0 +1,214 @@ +import { DialogRef } from '@angular/cdk/dialog'; +import { NgFor, NgStyle } from '@angular/common'; +import { Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { Content } from '@ngneat/overview'; +import { + CommandComponent, + InputDirective, + ListComponent, + GroupComponent, + ItemDirective, + EmptyDirective, + SeparatorComponent, +} from '@ngxpert/cmdk'; + +@Component({ + selector: 'app-jump-to-dialog', + templateUrl: 'jump-to-dialog.component.html', + standalone: true, + imports: [ + CommandComponent, + InputDirective, + ListComponent, + GroupComponent, + ItemDirective, + EmptyDirective, + SeparatorComponent, + NgStyle, + NgFor + ], +}) +export class JumpToDialogComponent { + readonly jumpSections: { href: string; emoji: string; label: string }[] = [ + { + href: '#info', + emoji: 'ℹ️', + label: 'Info', + }, + { + href: '#success', + emoji: 'βœ…', + label: 'Success', + }, + { + href: '#warning', + emoji: '⚠️', + label: 'Warning', + }, + { + href: '#error', + emoji: '❌', + label: 'Error', + }, + { + href: '#loader', + emoji: 'πŸ”„οΈ', + label: 'Loader', + }, + { + href: '#observe', + emoji: '⏳', + label: 'Observe', + }, + { + href: '#multi', + emoji: '↕️', + label: 'Multi Line', + }, + { + href: '#emoji', + emoji: 'πŸ‘', + label: 'Emoji', + }, + { + href: '#snackbar', + emoji: '🌞', + label: 'Snackbar', + }, + { + href: '#dismissible', + emoji: '❎', + label: 'dismissible', + }, + { + href: '#events', + emoji: 'πŸ”‚', + label: 'Events', + }, + { + href: '#themed', + emoji: '🎨', + label: 'Themed', + }, + { + href: '#toast-ref', + emoji: 'πŸ•΅οΈ', + label: 'Close manually', + }, + { + href: '#toast-ref-msg', + emoji: 'πŸ•΅οΈ', + label: 'Update message', + }, + { + href: '#only-one-at-a-time', + emoji: '☝️', + label: 'One at a Time', + }, + { + href: '#persistent', + emoji: 'πŸ”’', + label: 'Persistent', + }, + { + href: '#html', + emoji: 'πŸ” ', + label: 'HTML', + }, + { + href: '#template', + emoji: 'πŸ”©', + label: 'Template', + }, + { + href: '#template-data', + emoji: '🎫', + label: 'Template Data', + }, + { + href: '#component', + emoji: 'πŸ†•', + label: 'Component', + }, + { + href: '#injector', + emoji: 'πŸ’‰', + label: 'Injector', + }, + { + href: '#component-data', + emoji: 'πŸ’Ύ', + label: 'Component Data', + }, + { + href: '#positions', + emoji: 'πŸ…ΏοΈ', + label: 'Positions', + }, + { + href: '#stacking', + emoji: 'πŸͺœ', + label: 'Stacking', + }, + { + href: '#grouping-pre', + emoji: 'πŸ””', + label: 'Pre Grouped', + }, + { + href: '#grouping-post', + emoji: 'πŸ””', + label: 'Post Grouped', + }, + { + href: '#order', + emoji: 'πŸ”€', + label: 'Order', + }, + ]; + + @ViewChild('cmdkCommand') cmdkCommand!: ElementRef; + private router = inject(Router); + public dialogRef: DialogRef; + inputValue = ''; + readonly groups: Array<{ + group: string; + items: Array<{ + label: string; + itemSelected?: () => void; + icon: Content; + separatorOnTop?: boolean; + }>; + }> = [ + { + group: 'Variants', + items: [ + { + label: 'Info', + itemSelected: () => { + this.router.navigateByUrl('#info'); + this.dialogRef.close(); + }, + icon: 'ℹ️', + }, + ], + }, + ]; + readonly projectItems = new Array(6); + styleTransform = ''; + setInputValue(ev: Event) { + this.inputValue = (ev.target as HTMLInputElement).value; + } + onKeyDown(ev: KeyboardEvent) { + if (ev.key === 'Enter') { + this.bounce(); + } + } + bounce() { + this.styleTransform = 'scale(0.96)'; + setTimeout(() => { + this.styleTransform = ''; + }, 100); + } +} diff --git a/src/index.html b/src/index.html index 30093a0..e6135bc 100644 --- a/src/index.html +++ b/src/index.html @@ -15,6 +15,7 @@ /> + diff --git a/src/styles.scss b/src/styles.scss index 943903f..a253357 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -2,6 +2,8 @@ @use './styles/main.css'; @use '../projects/ngxpert/hot-toast/src/styles/styles.scss'; @use "./styles/grouping.css"; +// @use "~@ngxpert/cmdk/styles/scss/globals"; +// @use "~@ngxpert/cmdk/styles/scss/vercel"; .copy-to-clipboard-button { position: absolute; From 8b7b09f50e9e1f9189a6a562415e849da1df3f13 Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Wed, 17 Apr 2024 11:41:00 +0530 Subject: [PATCH 7/9] feat: remove first child div from .hot-toast-message to keep structure simple, removed div around ng-container in .hot-toast-message BREAKING CHANGE: a div around ng-container in .hot-toast-message has been removed. User will need to take care of this in their ng-template --- src/app/sections/grouping/snippets.ts | 62 ++++++++++++++------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/app/sections/grouping/snippets.ts b/src/app/sections/grouping/snippets.ts index ccc5edb..1b1ece3 100644 --- a/src/app/sections/grouping/snippets.ts +++ b/src/app/sections/grouping/snippets.ts @@ -2,66 +2,66 @@ const childNotifications = ` readonly commonOptions: ToastOptions<unknown> = { autoClose: false }; readonly childNotifications = (ngTemplateGroupItem: Content): HotToastGroupChild[] => [ -{ + { options: { - message: ngTemplateGroupItem, - data: { + message: ngTemplateGroupItem, + data: { title: 'New Message!', subTitle: 'Sarah sent you a message.', time: 'Just Now', icon: 'πŸ—¨οΈ', + }, + ...this.commonOptions, }, - ...this.commonOptions, - }, -}, -{ + }, + { options: { - message: ngTemplateGroupItem, - data: { + message: ngTemplateGroupItem, + data: { title: 'Level Up!', subTitle: "You've unlocked a new achievement.", time: '2 min ago', icon: 'βœ…', + }, + ...this.commonOptions, }, - ...this.commonOptions, - }, -}, -{ + }, + { options: { - message: ngTemplateGroupItem, - data: { + message: ngTemplateGroupItem, + data: { title: 'Reminder: Meeting Today', subTitle: 'Your team meeting starts in 30 minutes.', time: '1 hours ago', icon: '⏰', + }, + ...this.commonOptions, }, - ...this.commonOptions, - }, -}, -{ + }, + { options: { - message: ngTemplateGroupItem, - data: { + message: ngTemplateGroupItem, + data: { title: 'Special Offer!', subTitle: 'Save 20% off on subscription upgrade.', time: '12 hours ago', icon: '🏷️', + }, + ...this.commonOptions, }, - ...this.commonOptions, - }, -}, -{ + }, + { options: { - message: ngTemplateGroupItem, - data: { + message: ngTemplateGroupItem, + data: { title: 'Task Assigned', subTitle: 'A new task is awaiting your action.', time: 'Yesterday', icon: 'βœ”οΈ', + }, + ...this.commonOptions, }, - ...this.commonOptions, - }, -}, + }, ];`; export const preGroupingTS = ` @@ -126,12 +126,14 @@ export const preGroupingHTML = ` @if (visibleToasts(toastRef.groupRefs) === 0) { You're all caught up! } @else { What's happening around you! } </div> </div> + @if (visibleToasts(toastRef.groupRefs) > 0) { <button (click)="toastRef.toggleGroup()" class="ml-auto self-center hot-toast-group-btn" [class.expanded]="toastRef.groupExpanded" [attr.aria-label]="toastRef.groupExpanded ? 'Collapse' : 'Expand'" ></button> + } </div> </ng-template> <ng-template #groupItemTemplate let-toastRef> From 1028ac6d2e52af3333b26390d18f1342221806ed Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 17 Apr 2024 06:33:25 +0000 Subject: [PATCH 8/9] chore(release): 2.0.0-beta.1 [skip ci] # [2.0.0-beta.1](https://github.com/ngxpert/hot-toast/compare/v1.0.0...v2.0.0-beta.1) (2024-04-17) ### Bug Fixes * handle expanded in doCheck ([58a188a](https://github.com/ngxpert/hot-toast/commit/58a188a2b976fc43295f92858897700b0c9836d5)) * make options optional in show function ([8ecbbc2](https://github.com/ngxpert/hot-toast/commit/8ecbbc2a3c10b6e3006debd5aad7e304c7984d71)) ### Features * add grouping feature ([c342de6](https://github.com/ngxpert/hot-toast/commit/c342de69adcf246a30e51f01c5a411a822756e3e)) * post grouping ([bc32842](https://github.com/ngxpert/hot-toast/commit/bc32842f04b7f997cbefb98d2ad217941652b2bb)) * remove first child div from .hot-toast-message ([8b7b09f](https://github.com/ngxpert/hot-toast/commit/8b7b09f50e9e1f9189a6a562415e849da1df3f13)) ### BREAKING CHANGES * a div around ng-container in .hot-toast-message has been removed. User will need to take care of this in their ng-template --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb44af4..2991de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# [2.0.0-beta.1](https://github.com/ngxpert/hot-toast/compare/v1.0.0...v2.0.0-beta.1) (2024-04-17) + + +### Bug Fixes + +* handle expanded in doCheck ([58a188a](https://github.com/ngxpert/hot-toast/commit/58a188a2b976fc43295f92858897700b0c9836d5)) +* make options optional in show function ([8ecbbc2](https://github.com/ngxpert/hot-toast/commit/8ecbbc2a3c10b6e3006debd5aad7e304c7984d71)) + + +### Features + +* add grouping feature ([c342de6](https://github.com/ngxpert/hot-toast/commit/c342de69adcf246a30e51f01c5a411a822756e3e)) +* post grouping ([bc32842](https://github.com/ngxpert/hot-toast/commit/bc32842f04b7f997cbefb98d2ad217941652b2bb)) +* remove first child div from .hot-toast-message ([8b7b09f](https://github.com/ngxpert/hot-toast/commit/8b7b09f50e9e1f9189a6a562415e849da1df3f13)) + + +### BREAKING CHANGES + +* a div around ng-container in .hot-toast-message has been removed. User will need to +take care of this in their ng-template + # 1.0.0 (2024-02-19) From d111bc68a6cd2704ce358c5c514c211926ec51d0 Mon Sep 17 00:00:00 2001 From: Dharmen Shah Date: Wed, 17 Apr 2024 12:34:19 +0530 Subject: [PATCH 9/9] docs: add migration guide --- README.md | 8 +++++++- src/app/sections/grouping/snippets.ts | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4317cc..23a03f2 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ https://github.com/ngxpert/hot-toast/assets/6831283/ae718568-d5ea-47bf-a41d-6aab - 1.x + 1.x, 2.x >= 17 @@ -297,6 +297,12 @@ Focus is not, and should not be, moved to the hot-toast element. Moving the focu Hot-toasts that have an [action available](https://ngxpert.github.io/hot-toast/#template) should be set `autoClose: false`, as to accommodate screen-reader users that want to navigate to the hot-toast element to activate the action. +## Breaking Changes + +### v1 to v2 + +The `
` surrounding `` is removed from `.hot-toast-message` to better and easy structure of layout. User may need to check their templates after updating to v2. + ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/src/app/sections/grouping/snippets.ts b/src/app/sections/grouping/snippets.ts index 1b1ece3..0915a72 100644 --- a/src/app/sections/grouping/snippets.ts +++ b/src/app/sections/grouping/snippets.ts @@ -192,7 +192,8 @@ export const preGroupingCSS = ` .bell-animation { animation-duration: 1s; - animation-iteration-count: var(--ring-bell); + animation-iteration-count: 1; + animation-delay: 300ms; animation-name: bell-keyframes; display: inline-block; transform-origin: top center;