mn-angular-lib 1.0.140 → 1.0.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mn-angular-lib.mjs +445 -215
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/features/mn-bottom-sheet/mn-bottom-sheet.component.css +76 -0
- package/src/lib/features/mn-modal/components/mn-modal-shell/mn-modal-shell.component.css +13 -113
- package/src/lib/features/mn-multi-select/mn-multi-select.css +4 -54
- package/types/mn-angular-lib.d.ts +194 -64
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, inject, HostBinding, Input, Component, ApplicationRef, APP_INITIALIZER, Pipe, ChangeDetectionStrategy, signal, DestroyRef, Optional, SkipSelf, Attribute, Directive, ElementRef, EventEmitter, computed, Output, input, output, Injector, viewChildren, linkedSignal, afterNextRender,
|
|
2
|
+
import { InjectionToken, Injectable, inject, HostBinding, Input, Component, ApplicationRef, APP_INITIALIZER, Pipe, ChangeDetectionStrategy, signal, DestroyRef, Optional, SkipSelf, Attribute, Directive, ElementRef, EventEmitter, computed, Output, input, output, Injector, viewChildren, linkedSignal, afterNextRender, ChangeDetectorRef, viewChild, Renderer2, HostListener, ViewChild, ViewContainerRef, forwardRef, afterEveryRender, TemplateRef, ViewChildren, EnvironmentInjector, createComponent, isSignal, effect, untracked, ViewEncapsulation } from '@angular/core';
|
|
3
3
|
export { TemplateRef, Type } from '@angular/core';
|
|
4
4
|
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
@@ -3491,6 +3491,272 @@ const mnMultiSelectVariants = tv({
|
|
|
3491
3491
|
},
|
|
3492
3492
|
});
|
|
3493
3493
|
|
|
3494
|
+
/**
|
|
3495
|
+
* A viewport-anchored bottom sheet — the mobile presentation shared by the modal
|
|
3496
|
+
* shell and the multi-select dropdown.
|
|
3497
|
+
*
|
|
3498
|
+
* It owns only the sheet *chrome and gestures*: a bottom-anchored, full-width,
|
|
3499
|
+
* rounded-top surface with an optional dimming backdrop and drag grabber, a
|
|
3500
|
+
* slide-up entrance, swipe/flick-to-dismiss, and a promise-based slide-down exit.
|
|
3501
|
+
* It is deliberately presentational: the body is projected via `<ng-content>`, and
|
|
3502
|
+
* dismissal is reported through {@link dismiss} for the host to act on (run a close
|
|
3503
|
+
* guard, tear down its overlay, …) rather than being handled here.
|
|
3504
|
+
*
|
|
3505
|
+
* Positioning is `position: fixed` against the viewport, so a consumer whose sheet
|
|
3506
|
+
* lives inside a `transform`/`filter` ancestor (which would otherwise become the
|
|
3507
|
+
* containing block) must relocate this host to `document.body` — as the
|
|
3508
|
+
* multi-select does with its portal helper.
|
|
3509
|
+
*/
|
|
3510
|
+
class MnBottomSheet {
|
|
3511
|
+
/** Tailwind's `sm` breakpoint — at or below this the swipe gesture is armed.
|
|
3512
|
+
* Kept in step with the same constant in the modal shell and multi-select. */
|
|
3513
|
+
static SHEET_MAX_WIDTH = 639.98;
|
|
3514
|
+
/** Drag distance (px) past which a release dismisses regardless of speed. */
|
|
3515
|
+
static SWIPE_DISMISS_THRESHOLD = 150;
|
|
3516
|
+
/** Downward release speed (px/ms) above which a short drag still dismisses — a "flick". */
|
|
3517
|
+
static FLICK_VELOCITY = 0.5;
|
|
3518
|
+
/** Minimum drag distance (px) a flick must cover, so an incidental fast tap never dismisses. */
|
|
3519
|
+
static FLICK_MIN_DISTANCE = 32;
|
|
3520
|
+
/** Upper bound for the exit wait if no `transitionend` fires (e.g. animation suppressed). */
|
|
3521
|
+
static CLOSE_FALLBACK_MS = 700;
|
|
3522
|
+
/** Whether to render the dimming backdrop behind the sheet (default: true).
|
|
3523
|
+
* A host that already paints its own backdrop (the modal shell) sets this false. */
|
|
3524
|
+
showBackdrop = true;
|
|
3525
|
+
/** Whether to render the drag grabber handle that arms swipe-to-dismiss (default: true). */
|
|
3526
|
+
showGrabber = true;
|
|
3527
|
+
/** Whether the sheet can be dismissed by the user via swipe/flick or backdrop tap
|
|
3528
|
+
* (default: true). When false the gestures are inert and the backdrop is non-closing. */
|
|
3529
|
+
dismissible = true;
|
|
3530
|
+
/** Optional `min-height` floor (px) for the container, so filtering its content
|
|
3531
|
+
* shorter cannot shrink the sheet mid-interaction. Null leaves it content-sized. */
|
|
3532
|
+
minHeightPx = null;
|
|
3533
|
+
/** Cap on the sheet height as a fraction of the viewport, in vh (default: 80). */
|
|
3534
|
+
maxHeightVh = 80;
|
|
3535
|
+
/** Extra class(es) applied to the sheet container, so a host can attach the hooks
|
|
3536
|
+
* its own CSS depends on (e.g. the modal shell's `modal-container`). */
|
|
3537
|
+
containerClass = '';
|
|
3538
|
+
/** Accessible name for the sheet dialog. */
|
|
3539
|
+
ariaLabel;
|
|
3540
|
+
/** Id of the element that labels this dialog (takes precedence over `ariaLabel`). */
|
|
3541
|
+
ariaLabelledby;
|
|
3542
|
+
/**
|
|
3543
|
+
* When true, the sheet grows to its `maxHeightVh` while the host app marks the soft
|
|
3544
|
+
* keyboard open (a `.mn-keyboard-open` class on a document ancestor), guaranteeing
|
|
3545
|
+
* scroll room to lift a focused field above an overlaying keyboard. Off by default so
|
|
3546
|
+
* a keyboard opened over an unrelated sheet (a multi-select search) does not resize it.
|
|
3547
|
+
*/
|
|
3548
|
+
growWithKeyboard = false;
|
|
3549
|
+
/**
|
|
3550
|
+
* Optional async gate consulted before a user-initiated dismissal (swipe/flick/backdrop
|
|
3551
|
+
* tap) is committed. Resolving false aborts the dismissal and springs the sheet back —
|
|
3552
|
+
* used by the modal to run its close guard (e.g. an unsaved-changes prompt). A
|
|
3553
|
+
* programmatic {@link startClosing} bypasses it.
|
|
3554
|
+
*/
|
|
3555
|
+
dismissGuard;
|
|
3556
|
+
/**
|
|
3557
|
+
* Emitted once the user has dismissed the sheet — after the slide-down exit has
|
|
3558
|
+
* finished, so the host can remove the sheet from the DOM without cutting the
|
|
3559
|
+
* animation short. The host decides what dismissal means (close, run a guard, …).
|
|
3560
|
+
*/
|
|
3561
|
+
dismiss = new EventEmitter();
|
|
3562
|
+
/** Current downward drag offset (px) applied to the sheet while swiping. */
|
|
3563
|
+
sheetDragY = 0;
|
|
3564
|
+
/** True while the user is actively dragging the grabber (disables the snap transition). */
|
|
3565
|
+
isDraggingSheet = false;
|
|
3566
|
+
/** True once a dismissal has committed — the sheet glides off-screen via its transition. */
|
|
3567
|
+
isDismissing = false;
|
|
3568
|
+
cdr = inject(ChangeDetectorRef);
|
|
3569
|
+
el = inject(ElementRef);
|
|
3570
|
+
/** The sheet container element, used to measure its height and drive the exit. */
|
|
3571
|
+
containerRef = viewChild('container', ...(ngDevMode ? [{ debugName: "containerRef" }] : []));
|
|
3572
|
+
dragStartY = 0;
|
|
3573
|
+
/** The two most recent (y, timestamp) pointer samples, for estimating flick velocity.
|
|
3574
|
+
* `t` uses the event timestamp (monotonic), so no wall-clock is read. */
|
|
3575
|
+
lastSample = null;
|
|
3576
|
+
prevSample = null;
|
|
3577
|
+
/** In-flight exit animation, so a swipe-dismiss and a follow-up programmatic
|
|
3578
|
+
* {@link startClosing} share one glide instead of re-triggering it. */
|
|
3579
|
+
exitPromise = null;
|
|
3580
|
+
get hostClasses() {
|
|
3581
|
+
return `mn-bottom-sheet${this.isDismissing ? ' is-dismissing' : ''}`
|
|
3582
|
+
+ `${this.growWithKeyboard ? ' grow-with-keyboard' : ''}`;
|
|
3583
|
+
}
|
|
3584
|
+
/** Whether the viewport is currently narrow enough for the sheet to accept a swipe. */
|
|
3585
|
+
get isNarrow() {
|
|
3586
|
+
return typeof window === 'undefined' || window.innerWidth <= MnBottomSheet.SHEET_MAX_WIDTH;
|
|
3587
|
+
}
|
|
3588
|
+
onSheetPointerDown(event) {
|
|
3589
|
+
if (!this.dismissible || !this.isNarrow)
|
|
3590
|
+
return;
|
|
3591
|
+
// Don't hijack drags that begin on an interactive control inside the sheet.
|
|
3592
|
+
if (event.target.closest('button, input, textarea, select, a'))
|
|
3593
|
+
return;
|
|
3594
|
+
this.isDraggingSheet = true;
|
|
3595
|
+
this.dragStartY = event.clientY;
|
|
3596
|
+
// Seed the velocity window so a fast flick that releases on the first move still measures.
|
|
3597
|
+
this.lastSample = { y: event.clientY, t: event.timeStamp };
|
|
3598
|
+
this.prevSample = this.lastSample;
|
|
3599
|
+
event.target.setPointerCapture(event.pointerId);
|
|
3600
|
+
}
|
|
3601
|
+
onSheetPointerMove(event) {
|
|
3602
|
+
if (!this.isDraggingSheet)
|
|
3603
|
+
return;
|
|
3604
|
+
// Only track downward movement.
|
|
3605
|
+
this.sheetDragY = Math.max(0, event.clientY - this.dragStartY);
|
|
3606
|
+
this.prevSample = this.lastSample;
|
|
3607
|
+
this.lastSample = { y: event.clientY, t: event.timeStamp };
|
|
3608
|
+
}
|
|
3609
|
+
onSheetPointerUp() {
|
|
3610
|
+
if (!this.isDraggingSheet)
|
|
3611
|
+
return;
|
|
3612
|
+
this.isDraggingSheet = false;
|
|
3613
|
+
if (this.shouldDismiss()) {
|
|
3614
|
+
void this.attemptDismiss();
|
|
3615
|
+
}
|
|
3616
|
+
else {
|
|
3617
|
+
this.snapBack();
|
|
3618
|
+
}
|
|
3619
|
+
this.lastSample = null;
|
|
3620
|
+
this.prevSample = null;
|
|
3621
|
+
}
|
|
3622
|
+
onBackdropClick() {
|
|
3623
|
+
void this.attemptDismiss();
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Plays the slide-down exit and resolves once it has finished. Exposed so a host that
|
|
3627
|
+
* dismisses the sheet programmatically (not via a gesture) can await the same exit
|
|
3628
|
+
* before tearing the sheet down. Idempotent: a swipe-dismiss already in flight and a
|
|
3629
|
+
* subsequent programmatic close share the one glide rather than restarting it.
|
|
3630
|
+
*/
|
|
3631
|
+
startClosing() {
|
|
3632
|
+
return this.playExit();
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Runs the optional {@link dismissGuard}, then either commits the dismissal (glide out
|
|
3636
|
+
* + emit) or springs the sheet back if the guard rejects. A non-dismissible sheet never
|
|
3637
|
+
* gets here from a gesture, but the guard is still short-circuited defensively.
|
|
3638
|
+
*/
|
|
3639
|
+
async attemptDismiss() {
|
|
3640
|
+
if (!this.dismissible)
|
|
3641
|
+
return;
|
|
3642
|
+
if (this.dismissGuard) {
|
|
3643
|
+
const allowed = await this.dismissGuard();
|
|
3644
|
+
if (!allowed) {
|
|
3645
|
+
this.snapBack();
|
|
3646
|
+
this.cdr.detectChanges();
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
this.commitDismiss();
|
|
3651
|
+
}
|
|
3652
|
+
/** Whether the release should dismiss: a long-enough drag OR a fast downward flick. */
|
|
3653
|
+
shouldDismiss() {
|
|
3654
|
+
if (this.sheetDragY > MnBottomSheet.SWIPE_DISMISS_THRESHOLD) {
|
|
3655
|
+
return true;
|
|
3656
|
+
}
|
|
3657
|
+
return this.releaseVelocity() > MnBottomSheet.FLICK_VELOCITY
|
|
3658
|
+
&& this.sheetDragY > MnBottomSheet.FLICK_MIN_DISTANCE;
|
|
3659
|
+
}
|
|
3660
|
+
/** Downward release speed (px/ms) from the last two pointer samples; 0 when unusable. */
|
|
3661
|
+
releaseVelocity() {
|
|
3662
|
+
if (!this.lastSample || !this.prevSample)
|
|
3663
|
+
return 0;
|
|
3664
|
+
const dt = this.lastSample.t - this.prevSample.t;
|
|
3665
|
+
if (dt <= 0)
|
|
3666
|
+
return 0;
|
|
3667
|
+
return (this.lastSample.y - this.prevSample.y) / dt;
|
|
3668
|
+
}
|
|
3669
|
+
/** Springs the sheet back to its resting position after a drag that didn't dismiss. */
|
|
3670
|
+
snapBack() {
|
|
3671
|
+
this.sheetDragY = 0;
|
|
3672
|
+
}
|
|
3673
|
+
/**
|
|
3674
|
+
* Commits a dismissal: glides the sheet the rest of the way off-screen, then emits
|
|
3675
|
+
* {@link dismiss} once the exit animation settles. Continuing the gesture (rather than
|
|
3676
|
+
* snapping back to 0 first) keeps a swipe feeling like one unbroken motion.
|
|
3677
|
+
*/
|
|
3678
|
+
commitDismiss() {
|
|
3679
|
+
void this.playExit().then(() => this.dismiss.emit());
|
|
3680
|
+
}
|
|
3681
|
+
/** Commits the exit animation exactly once and returns the shared in-flight promise.
|
|
3682
|
+
* Short-circuits under reduced motion and falls back to a timeout if no event fires. */
|
|
3683
|
+
playExit() {
|
|
3684
|
+
if (this.exitPromise)
|
|
3685
|
+
return this.exitPromise;
|
|
3686
|
+
this.isDismissing = true;
|
|
3687
|
+
this.sheetDragY = window.innerHeight;
|
|
3688
|
+
this.cdr.detectChanges();
|
|
3689
|
+
this.exitPromise = this.awaitExit();
|
|
3690
|
+
return this.exitPromise;
|
|
3691
|
+
}
|
|
3692
|
+
/** Waits for the container's exit transition to end, with a reduced-motion short-circuit
|
|
3693
|
+
* and a fallback timeout so it always resolves. */
|
|
3694
|
+
awaitExit() {
|
|
3695
|
+
return new Promise(resolve => {
|
|
3696
|
+
if (this.prefersReducedMotion()) {
|
|
3697
|
+
resolve();
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
const container = this.containerRef()?.nativeElement
|
|
3701
|
+
?? this.el.nativeElement.querySelector('.mn-sheet-container');
|
|
3702
|
+
if (!container) {
|
|
3703
|
+
resolve();
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
let settled = false;
|
|
3707
|
+
const done = (event) => {
|
|
3708
|
+
// Ignore end events bubbling up from descendant transitions.
|
|
3709
|
+
if (event && event.target !== container)
|
|
3710
|
+
return;
|
|
3711
|
+
if (settled)
|
|
3712
|
+
return;
|
|
3713
|
+
settled = true;
|
|
3714
|
+
container.removeEventListener('transitionend', done);
|
|
3715
|
+
clearTimeout(fallback);
|
|
3716
|
+
resolve();
|
|
3717
|
+
};
|
|
3718
|
+
container.addEventListener('transitionend', done);
|
|
3719
|
+
const fallback = setTimeout(done, MnBottomSheet.CLOSE_FALLBACK_MS);
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
prefersReducedMotion() {
|
|
3723
|
+
return typeof window !== 'undefined'
|
|
3724
|
+
&& typeof window.matchMedia === 'function'
|
|
3725
|
+
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
3726
|
+
}
|
|
3727
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnBottomSheet, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3728
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnBottomSheet, isStandalone: true, selector: "mn-bottom-sheet", inputs: { showBackdrop: "showBackdrop", showGrabber: "showGrabber", dismissible: "dismissible", minHeightPx: "minHeightPx", maxHeightVh: "maxHeightVh", containerClass: "containerClass", ariaLabel: "ariaLabel", ariaLabelledby: "ariaLabelledby", growWithKeyboard: "growWithKeyboard", dismissGuard: "dismissGuard" }, outputs: { dismiss: "dismiss" }, host: { properties: { "class": "this.hostClasses" } }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (showBackdrop) {\n <!-- Dims the page behind the sheet. Tapping it dismisses when the sheet is dismissible. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"onBackdropClick()\" class=\"mn-sheet-backdrop fixed inset-0 z-9998 bg-black/40\"></div>\n}\n\n<div\n #container\n [attr.aria-label]=\"ariaLabelledby ? null : (ariaLabel || null)\"\n [attr.aria-labelledby]=\"ariaLabelledby || null\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerClass\"\n [style.--mn-sheet-max]=\"maxHeightVh + 'vh'\"\n [style.max-height.vh]=\"maxHeightVh\"\n [style.min-height.px]=\"minHeightPx\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n aria-modal=\"true\"\n class=\"mn-sheet-container fixed inset-x-0 bottom-0 z-9999 flex flex-col bg-base-100 border-t border-base-300 rounded-t-2xl shadow-lg\"\n role=\"dialog\"\n tabindex=\"-1\"\n>\n @if (showGrabber) {\n <!-- Drag handle for swipe-to-dismiss. The gesture is armed on the whole handle;\n drags starting on a control inside the projected content are ignored. -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"flex justify-center pt-2 pb-1 touch-none cursor-grab shrink-0\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <ng-content></ng-content>\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);display:contents}.mn-sheet-container{padding-bottom:env(safe-area-inset-bottom);min-height:0;transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease);animation:mn-sheet-in .35s var(--mn-sheet-ease)}:host(.grow-with-keyboard):host-context(.mn-keyboard-open) .mn-sheet-container{min-height:var(--mn-sheet-max, 92vh)}.mn-sheet-container.sheet-dragging{transition:none}.mn-sheet-backdrop{animation:mn-sheet-backdrop-in .2s ease-out}:host(.is-dismissing) .mn-sheet-container{animation:none}@keyframes mn-sheet-in{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes mn-sheet-backdrop-in{0%{opacity:0}to{opacity:1}}@media(prefers-reduced-motion:reduce){.mn-sheet-container,.mn-sheet-backdrop{animation-duration:.01ms!important;transition-duration:.01ms!important}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
|
|
3729
|
+
}
|
|
3730
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnBottomSheet, decorators: [{
|
|
3731
|
+
type: Component,
|
|
3732
|
+
args: [{ selector: 'mn-bottom-sheet', standalone: true, imports: [NgClass], template: "@if (showBackdrop) {\n <!-- Dims the page behind the sheet. Tapping it dismisses when the sheet is dismissible. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"onBackdropClick()\" class=\"mn-sheet-backdrop fixed inset-0 z-9998 bg-black/40\"></div>\n}\n\n<div\n #container\n [attr.aria-label]=\"ariaLabelledby ? null : (ariaLabel || null)\"\n [attr.aria-labelledby]=\"ariaLabelledby || null\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerClass\"\n [style.--mn-sheet-max]=\"maxHeightVh + 'vh'\"\n [style.max-height.vh]=\"maxHeightVh\"\n [style.min-height.px]=\"minHeightPx\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n aria-modal=\"true\"\n class=\"mn-sheet-container fixed inset-x-0 bottom-0 z-9999 flex flex-col bg-base-100 border-t border-base-300 rounded-t-2xl shadow-lg\"\n role=\"dialog\"\n tabindex=\"-1\"\n>\n @if (showGrabber) {\n <!-- Drag handle for swipe-to-dismiss. The gesture is armed on the whole handle;\n drags starting on a control inside the projected content are ignored. -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"flex justify-center pt-2 pb-1 touch-none cursor-grab shrink-0\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <ng-content></ng-content>\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);display:contents}.mn-sheet-container{padding-bottom:env(safe-area-inset-bottom);min-height:0;transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease);animation:mn-sheet-in .35s var(--mn-sheet-ease)}:host(.grow-with-keyboard):host-context(.mn-keyboard-open) .mn-sheet-container{min-height:var(--mn-sheet-max, 92vh)}.mn-sheet-container.sheet-dragging{transition:none}.mn-sheet-backdrop{animation:mn-sheet-backdrop-in .2s ease-out}:host(.is-dismissing) .mn-sheet-container{animation:none}@keyframes mn-sheet-in{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes mn-sheet-backdrop-in{0%{opacity:0}to{opacity:1}}@media(prefers-reduced-motion:reduce){.mn-sheet-container,.mn-sheet-backdrop{animation-duration:.01ms!important;transition-duration:.01ms!important}}\n"] }]
|
|
3733
|
+
}], propDecorators: { showBackdrop: [{
|
|
3734
|
+
type: Input
|
|
3735
|
+
}], showGrabber: [{
|
|
3736
|
+
type: Input
|
|
3737
|
+
}], dismissible: [{
|
|
3738
|
+
type: Input
|
|
3739
|
+
}], minHeightPx: [{
|
|
3740
|
+
type: Input
|
|
3741
|
+
}], maxHeightVh: [{
|
|
3742
|
+
type: Input
|
|
3743
|
+
}], containerClass: [{
|
|
3744
|
+
type: Input
|
|
3745
|
+
}], ariaLabel: [{
|
|
3746
|
+
type: Input
|
|
3747
|
+
}], ariaLabelledby: [{
|
|
3748
|
+
type: Input
|
|
3749
|
+
}], growWithKeyboard: [{
|
|
3750
|
+
type: Input
|
|
3751
|
+
}], dismissGuard: [{
|
|
3752
|
+
type: Input
|
|
3753
|
+
}], dismiss: [{
|
|
3754
|
+
type: Output
|
|
3755
|
+
}], containerRef: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], hostClasses: [{
|
|
3756
|
+
type: HostBinding,
|
|
3757
|
+
args: ['class']
|
|
3758
|
+
}] } });
|
|
3759
|
+
|
|
3494
3760
|
const MN_MULTI_SELECT_CONFIG = new InjectionToken('MN_MULTI_SELECT_CONFIG');
|
|
3495
3761
|
class MnMultiSelect {
|
|
3496
3762
|
ngControl = inject(NgControl, { optional: true, self: true });
|
|
@@ -3506,10 +3772,11 @@ class MnMultiSelect {
|
|
|
3506
3772
|
cdr = inject(ChangeDetectorRef);
|
|
3507
3773
|
/** Reference to the trigger element for positioning the dropdown */
|
|
3508
3774
|
triggerRef;
|
|
3509
|
-
/**
|
|
3775
|
+
/** Layout classes for the anchored popover panel. The mobile sheet is rendered by
|
|
3776
|
+
* mn-bottom-sheet instead, so it no longer needs a branch here. */
|
|
3777
|
+
panelClasses = 'fixed z-9999 bg-base-100 border border-base-300 rounded-md shadow-lg max-h-60 overflow-auto';
|
|
3778
|
+
/** The anchored popover panel currently moved into `document.body`, if any. */
|
|
3510
3779
|
movedPanel = null;
|
|
3511
|
-
/** The sheet backdrop element currently moved into `document.body`, if any. */
|
|
3512
|
-
movedBackdrop = null;
|
|
3513
3780
|
/** Option count at which the search input auto-enables when `searchable` is unset. */
|
|
3514
3781
|
static DEFAULT_SEARCH_THRESHOLD = 8;
|
|
3515
3782
|
/** Tailwind's `sm` breakpoint — below this the panel renders as a bottom sheet.
|
|
@@ -3542,6 +3809,8 @@ class MnMultiSelect {
|
|
|
3542
3809
|
* card) used to leave the portalled panel floating at its stale coordinates.
|
|
3543
3810
|
*/
|
|
3544
3811
|
scrollCapture = null;
|
|
3812
|
+
/** The bottom-sheet host currently moved into `document.body`, if any. */
|
|
3813
|
+
movedSheet = null;
|
|
3545
3814
|
/**
|
|
3546
3815
|
* The dropdown panel element, queried while it is rendered by the `@if` block.
|
|
3547
3816
|
* The setter relocates the panel to `document.body` so that its `position: fixed`
|
|
@@ -3551,22 +3820,7 @@ class MnMultiSelect {
|
|
|
3551
3820
|
* broken on iOS). Cleanup is handled when the query clears on close/destroy.
|
|
3552
3821
|
*/
|
|
3553
3822
|
set dropdownRef(ref) {
|
|
3554
|
-
|
|
3555
|
-
this.movedPanel = this.portal(el, this.movedPanel);
|
|
3556
|
-
if (el && this.isSheet) {
|
|
3557
|
-
this.captureSheetFloor(el);
|
|
3558
|
-
}
|
|
3559
|
-
else if (!el) {
|
|
3560
|
-
this.sheetFloorPx = null;
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
/**
|
|
3564
|
-
* The dimming backdrop rendered behind the mobile sheet. Portalled alongside the
|
|
3565
|
-
* panel for the same reason — a `position: fixed` backdrop left inside a transformed
|
|
3566
|
-
* ancestor would cover that ancestor rather than the viewport.
|
|
3567
|
-
*/
|
|
3568
|
-
set sheetBackdropRef(ref) {
|
|
3569
|
-
this.movedBackdrop = this.portal(ref?.nativeElement ?? null, this.movedBackdrop);
|
|
3823
|
+
this.movedPanel = this.portal(ref?.nativeElement ?? null, this.movedPanel);
|
|
3570
3824
|
}
|
|
3571
3825
|
/** Currently selected values */
|
|
3572
3826
|
selectedValues = [];
|
|
@@ -3585,21 +3839,21 @@ class MnMultiSelect {
|
|
|
3585
3839
|
if (this.ngControl)
|
|
3586
3840
|
this.ngControl.valueAccessor = this;
|
|
3587
3841
|
}
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
this.
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
this.
|
|
3602
|
-
}
|
|
3842
|
+
/**
|
|
3843
|
+
* The bottom-sheet host, read as an `ElementRef` so it can be relocated to
|
|
3844
|
+
* `document.body` — its `position: fixed` children (backdrop + container) must anchor
|
|
3845
|
+
* to the viewport, not to any transformed/filtered ancestor of this component. On open
|
|
3846
|
+
* its container height is captured as the sheet's `min-height` floor.
|
|
3847
|
+
*/
|
|
3848
|
+
set sheetRef(ref) {
|
|
3849
|
+
const el = ref?.nativeElement ?? null;
|
|
3850
|
+
this.movedSheet = this.portal(el, this.movedSheet);
|
|
3851
|
+
if (el) {
|
|
3852
|
+
this.captureSheetFloor(el);
|
|
3853
|
+
}
|
|
3854
|
+
else {
|
|
3855
|
+
this.sheetFloorPx = null;
|
|
3856
|
+
}
|
|
3603
3857
|
}
|
|
3604
3858
|
/**
|
|
3605
3859
|
* Tracks the sheet breakpoint through `matchMedia` rather than reading `innerWidth`
|
|
@@ -3628,15 +3882,21 @@ class MnMultiSelect {
|
|
|
3628
3882
|
this.sheetMedia = null;
|
|
3629
3883
|
this.sheetMediaListener = null;
|
|
3630
3884
|
}
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3885
|
+
ngOnInit() {
|
|
3886
|
+
this.resolveConfig();
|
|
3887
|
+
this.startWatchingViewport();
|
|
3888
|
+
const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
|
|
3889
|
+
this.resolveConfig();
|
|
3890
|
+
});
|
|
3891
|
+
this.destroyRef.onDestroy(() => {
|
|
3892
|
+
sub.unsubscribe();
|
|
3893
|
+
this.stopWatchingTrigger();
|
|
3894
|
+
this.stopWatchingViewport();
|
|
3895
|
+
this.unlockBodyScroll();
|
|
3896
|
+
// Guarantee the portalled elements never outlive the component.
|
|
3897
|
+
this.movedPanel = this.portal(null, this.movedPanel);
|
|
3898
|
+
this.movedSheet = this.portal(null, this.movedSheet);
|
|
3899
|
+
});
|
|
3640
3900
|
}
|
|
3641
3901
|
resolveConfig() {
|
|
3642
3902
|
const instanceId = this.explicitInstanceId || `mn-multi-select-${this.props.id}`;
|
|
@@ -3693,15 +3953,18 @@ class MnMultiSelect {
|
|
|
3693
3953
|
const threshold = this.props.searchThreshold ?? MnMultiSelect.DEFAULT_SEARCH_THRESHOLD;
|
|
3694
3954
|
return this.props.options.length >= threshold;
|
|
3695
3955
|
}
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
// The
|
|
3699
|
-
//
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3956
|
+
onDocumentClick(event) {
|
|
3957
|
+
const target = event.target;
|
|
3958
|
+
// The panel lives at the body root once open, so it is not a descendant of the
|
|
3959
|
+
// host element — treat clicks inside the portalled panel as "inside" too.
|
|
3960
|
+
const insideHost = !!target && this.elRef.nativeElement.contains(target);
|
|
3961
|
+
const insidePanel = !!target && !!this.movedPanel && this.movedPanel.contains(target);
|
|
3962
|
+
// In sheet mode the backdrop tap is handled by mn-bottom-sheet's own (dismiss); the
|
|
3963
|
+
// sheet host counts as "inside" here so this listener never double-fires the close.
|
|
3964
|
+
const insideSheet = !!target && !!this.movedSheet && this.movedSheet.contains(target);
|
|
3965
|
+
if (!insideHost && !insidePanel && !insideSheet) {
|
|
3966
|
+
this.close();
|
|
3967
|
+
}
|
|
3705
3968
|
}
|
|
3706
3969
|
/**
|
|
3707
3970
|
* Records the sheet's opened height as its `min-height` floor. Measured on the next
|
|
@@ -3709,17 +3972,24 @@ class MnMultiSelect {
|
|
|
3709
3972
|
* empty on open) and never forces a reflow mid change-detection. The floor equals the
|
|
3710
3973
|
* content height at that instant, so applying it triggers no resize — it only stops a
|
|
3711
3974
|
* later, shorter filtered list from pulling the sheet down.
|
|
3975
|
+
*
|
|
3976
|
+
* `hostEl` is the portalled mn-bottom-sheet host (`display: contents`), so the height
|
|
3977
|
+
* is read from its `.mn-sheet-container` child rather than the host itself.
|
|
3712
3978
|
*/
|
|
3713
|
-
captureSheetFloor(
|
|
3979
|
+
captureSheetFloor(hostEl) {
|
|
3980
|
+
const measure = () => {
|
|
3981
|
+
const container = hostEl.querySelector('.mn-sheet-container');
|
|
3982
|
+
return container?.offsetHeight ?? hostEl.offsetHeight;
|
|
3983
|
+
};
|
|
3714
3984
|
if (typeof requestAnimationFrame !== 'function') {
|
|
3715
|
-
this.sheetFloorPx =
|
|
3985
|
+
this.sheetFloorPx = measure();
|
|
3716
3986
|
return;
|
|
3717
3987
|
}
|
|
3718
3988
|
requestAnimationFrame(() => {
|
|
3719
|
-
// The
|
|
3720
|
-
if (!this.isOpen || this.
|
|
3989
|
+
// The sheet may have closed before the frame ran; don't strand a stale floor.
|
|
3990
|
+
if (!this.isOpen || this.movedSheet !== hostEl)
|
|
3721
3991
|
return;
|
|
3722
|
-
this.sheetFloorPx =
|
|
3992
|
+
this.sheetFloorPx = measure();
|
|
3723
3993
|
this.cdr.markForCheck();
|
|
3724
3994
|
});
|
|
3725
3995
|
}
|
|
@@ -3994,11 +4264,11 @@ class MnMultiSelect {
|
|
|
3994
4264
|
});
|
|
3995
4265
|
}
|
|
3996
4266
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3997
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "sheetBackdropRef", first: true, predicate: ["sheetBackdrop"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md cursor-pointer\"\n (click)=\"removeOption(opt, $event)\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n <!-- Dims the page behind the sheet. Deliberately has no click handler: the host's\n `document:click` listener already treats anything outside the panel as a\n dismissal, so a second handler would only duplicate that path. -->\n @if (isSheet) {\n <div #sheetBackdrop class=\"mn-ms-sheet-backdrop fixed inset-0 z-9998 bg-black/40\"></div>\n }\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n [ngClass]=\"panelClasses\"\n [style.top]=\"isSheet ? null : dropdownStyle.top\"\n [style.left]=\"isSheet ? null : dropdownStyle.left\"\n [style.width]=\"isSheet ? null : dropdownStyle.width\"\n [style.min-height.px]=\"isSheet ? sheetFloorPx : null\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- The sheet covers its own trigger, so it needs a header to name the field and\n an explicit way out \u2014 tapping the trigger again is not reachable here. -->\n @if (isSheet) {\n <div class=\"flex items-center justify-between gap-x-2 px-4 pt-4 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"shrink-0 cursor-pointer\"\n (click)=\"close()\"\n [attr.aria-label]=\"uiConfig.closeLabel || 'Close'\"\n ><svg lucideX [size]=\"20\"></svg></button>\n </div>\n }\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"border-b border-base-300 shrink-0\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n (click)=\"$event.stopPropagation()\"\n >\n <mn-lib-input-field\n [ngModel]=\"searchTerm\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"onSearch($event)\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: props.searchPlaceholder || 'Search...',\n ariaLabel: props.searchPlaceholder || 'Search...',\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <!-- In sheet mode the panel is the flex column and this list is the scroller;\n anchored, the panel itself scrolls and this is a passthrough wrapper. -->\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n type=\"checkbox\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n tabindex=\"-1\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div class=\"text-base-content/50\" [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\">\n {{ uiConfig.noOptionsFound || 'No options found' }}\n </div>\n }\n </div>\n </div>\n }\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", styles: [".mn-ms-sheet{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);padding-bottom:env(safe-area-inset-bottom);animation:mn-ms-sheet-in .3s var(--mn-sheet-ease)}.mn-ms-sheet-backdrop{animation:mn-ms-backdrop-in .2s ease-out}@keyframes mn-ms-sheet-in{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes mn-ms-backdrop-in{0%{opacity:0}to{opacity:1}}@media(prefers-reduced-motion:reduce){.mn-ms-sheet,.mn-ms-sheet-backdrop{animation-duration:.01ms!important}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
|
|
4267
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md cursor-pointer\"\n (click)=\"removeOption(opt, $event)\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it needs a header to name the field and\n an explicit way out \u2014 tapping the trigger again is not reachable here. -->\n <div class=\"flex items-center justify-between gap-x-2 px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"shrink-0 cursor-pointer\"\n (click)=\"close()\"\n [attr.aria-label]=\"uiConfig.closeLabel || 'Close'\"\n ><svg lucideX [size]=\"20\"></svg></button>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: props.searchPlaceholder || 'Search...',\n ariaLabel: props.searchPlaceholder || 'Search...',\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ uiConfig.noOptionsFound || 'No options found' }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
|
|
3998
4268
|
}
|
|
3999
4269
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, decorators: [{
|
|
4000
4270
|
type: Component,
|
|
4001
|
-
args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, FormsModule, MnErrorMessage, MnButton, MnInputField, LucideX, LucideChevronDown], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md cursor-pointer\"\n (click)=\"removeOption(opt, $event)\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n <!--
|
|
4271
|
+
args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnErrorMessage, MnButton, MnInputField, MnBottomSheet, LucideX, LucideChevronDown], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md cursor-pointer\"\n (click)=\"removeOption(opt, $event)\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it needs a header to name the field and\n an explicit way out \u2014 tapping the trigger again is not reachable here. -->\n <div class=\"flex items-center justify-between gap-x-2 px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"shrink-0 cursor-pointer\"\n (click)=\"close()\"\n [attr.aria-label]=\"uiConfig.closeLabel || 'Close'\"\n ><svg lucideX [size]=\"20\"></svg></button>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: props.searchPlaceholder || 'Search...',\n ariaLabel: props.searchPlaceholder || 'Search...',\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ uiConfig.noOptionsFound || 'No options found' }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
4002
4272
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
4003
4273
|
type: Input,
|
|
4004
4274
|
args: [{ required: true }]
|
|
@@ -4008,9 +4278,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4008
4278
|
}], dropdownRef: [{
|
|
4009
4279
|
type: ViewChild,
|
|
4010
4280
|
args: ['dropdown', { static: false }]
|
|
4011
|
-
}],
|
|
4281
|
+
}], sheetRef: [{
|
|
4012
4282
|
type: ViewChild,
|
|
4013
|
-
args: ['
|
|
4283
|
+
args: ['sheet', { static: false, read: ElementRef }]
|
|
4014
4284
|
}], onDocumentClick: [{
|
|
4015
4285
|
type: HostListener,
|
|
4016
4286
|
args: ['document:click', ['$event']]
|
|
@@ -8566,9 +8836,6 @@ class MnModalShellComponent {
|
|
|
8566
8836
|
}
|
|
8567
8837
|
el = inject(ElementRef);
|
|
8568
8838
|
cdr = inject(ChangeDetectorRef);
|
|
8569
|
-
/** Downward release speed (px/ms) above which a short drag still dismisses — a "flick".
|
|
8570
|
-
* Native sheets dismiss on a quick flick regardless of distance, not just a long drag. */
|
|
8571
|
-
static FLICK_VELOCITY = 0.5;
|
|
8572
8839
|
config;
|
|
8573
8840
|
modalRef;
|
|
8574
8841
|
isClosing = false;
|
|
@@ -8583,6 +8850,8 @@ class MnModalShellComponent {
|
|
|
8583
8850
|
ModalKind = ModalKind;
|
|
8584
8851
|
/** The rendered wizard body, when this modal is a wizard — used to read the active step title. */
|
|
8585
8852
|
wizardBody = viewChild(MnWizardBodyComponent, ...(ngDevMode ? [{ debugName: "wizardBody" }] : []));
|
|
8853
|
+
/** Tailwind's `sm` breakpoint — below this the modal presents as a bottom sheet. */
|
|
8854
|
+
static SHEET_MAX_WIDTH = 639.98;
|
|
8586
8855
|
/**
|
|
8587
8856
|
* Title of the wizard's current step, or undefined for non-wizard modals.
|
|
8588
8857
|
* The template appends it to the modal title on small screens, where the
|
|
@@ -8593,19 +8862,42 @@ class MnModalShellComponent {
|
|
|
8593
8862
|
focusTrapListener = null;
|
|
8594
8863
|
pollingTimer = null;
|
|
8595
8864
|
pollAttempts = 0;
|
|
8596
|
-
|
|
8597
|
-
|
|
8865
|
+
/** Upper bound for the close wait if no animation/transition end event fires
|
|
8866
|
+
* (e.g. an animation was suppressed). Must stay longer than the slowest close
|
|
8867
|
+
* path so it never preempts. */
|
|
8868
|
+
static CLOSE_FALLBACK_MS = 700;
|
|
8869
|
+
/** Live match of the sheet breakpoint, so the modal switches between the centered dialog
|
|
8870
|
+
* and the bottom sheet when the viewport crosses it (e.g. an orientation change). */
|
|
8871
|
+
isNarrow = signal(false, ...(ngDevMode ? [{ debugName: "isNarrow" }] : []));
|
|
8872
|
+
/** The bottom sheet presenting this modal on mobile, absent on the desktop dialog path. */
|
|
8873
|
+
bottomSheet = viewChild(MnBottomSheet, ...(ngDevMode ? [{ debugName: "bottomSheet" }] : []));
|
|
8874
|
+
/** Optional native haptic engine. Absent on the web — every call is null-guarded. */
|
|
8875
|
+
haptics = inject(MN_HAPTICS, { optional: true });
|
|
8876
|
+
sheetMedia = null;
|
|
8877
|
+
sheetMediaListener = null;
|
|
8878
|
+
/** Whether this modal is allowed to present as a bottom sheet on small screens (default: true). */
|
|
8879
|
+
get isMobileSheet() {
|
|
8880
|
+
return this.config.mobileBottomSheet !== false;
|
|
8598
8881
|
}
|
|
8599
|
-
/**
|
|
8600
|
-
*
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8882
|
+
/** Whether the modal should currently render as a bottom sheet (mobile) rather than the
|
|
8883
|
+
* centered dialog (desktop). */
|
|
8884
|
+
get showMobileSheet() {
|
|
8885
|
+
return this.isMobileSheet && this.isNarrow();
|
|
8886
|
+
}
|
|
8887
|
+
get hostClasses() {
|
|
8888
|
+
const size = this.config.sizeWidth || ModalSize.MD;
|
|
8889
|
+
// `closing` is intentionally NOT derived here. startClosing() adds the
|
|
8890
|
+
// `.closing` class imperatively (classList.add) for reliable, zoneless-safe
|
|
8891
|
+
// application. Deriving it here as well flips the host class string after the
|
|
8892
|
+
// view has been checked (NG0100). Angular's class binding only manages the tokens
|
|
8893
|
+
// it emits, so it leaves the imperatively added `.closing` untouched.
|
|
8894
|
+
const animType = typeof this.config.animation === 'string'
|
|
8895
|
+
? this.config.animation
|
|
8896
|
+
: this.config.animation?.type || 'slide';
|
|
8897
|
+
const animation = ` anim-${animType}`;
|
|
8898
|
+
const stacked = this.isStacked() ? ' is-stacked' : '';
|
|
8899
|
+
const mobileSheet = this.showMobileSheet ? ' mobile-sheet' : '';
|
|
8900
|
+
return `modal-shell modal-${size}${animation}${stacked}${mobileSheet}`;
|
|
8609
8901
|
}
|
|
8610
8902
|
setupFocusTrap() {
|
|
8611
8903
|
this.focusTrapListener = (e) => {
|
|
@@ -8649,45 +8941,54 @@ class MnModalShellComponent {
|
|
|
8649
8941
|
asCustom(config) {
|
|
8650
8942
|
return config;
|
|
8651
8943
|
}
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8944
|
+
/** Whether the modal can be dismissed at all (drives the sheet's swipe/backdrop arming). */
|
|
8945
|
+
get canClose() {
|
|
8946
|
+
return this.config.closeMode !== CloseMode.DISABLED;
|
|
8947
|
+
}
|
|
8948
|
+
ngOnInit() {
|
|
8949
|
+
this.startWatchingViewport();
|
|
8950
|
+
this.startPollingIfConfigured();
|
|
8951
|
+
}
|
|
8952
|
+
ngOnDestroy() {
|
|
8953
|
+
this.removeFocusTrap();
|
|
8954
|
+
this.stopPolling();
|
|
8955
|
+
this.stopWatchingViewport();
|
|
8956
|
+
// Restore focus to previously focused element
|
|
8957
|
+
if (this.previouslyFocusedElement && typeof this.previouslyFocusedElement.focus === 'function') {
|
|
8958
|
+
this.previouslyFocusedElement.focus();
|
|
8959
|
+
}
|
|
8665
8960
|
}
|
|
8666
8961
|
/**
|
|
8667
8962
|
* Triggers the closing animation and resolves once it has actually finished.
|
|
8668
8963
|
*
|
|
8669
|
-
* Deferred via setTimeout to avoid NG0100 when called during a CD cycle.
|
|
8670
|
-
*
|
|
8671
|
-
*
|
|
8672
|
-
*
|
|
8673
|
-
*
|
|
8674
|
-
* short-circuit entirely under reduced motion (the CSS collapses to instant).
|
|
8964
|
+
* Deferred via setTimeout to avoid NG0100 when called during a CD cycle. On mobile the
|
|
8965
|
+
* exit is owned by the bottom sheet, so we delegate to its `startClosing()` (idempotent
|
|
8966
|
+
* with a swipe-dismiss already in flight); on desktop we wait for the dialog container's
|
|
8967
|
+
* `animationend`/`transitionend`. A fallback timeout guarantees resolution if no event
|
|
8968
|
+
* fires, and we short-circuit under reduced motion (the CSS collapses to instant).
|
|
8675
8969
|
*/
|
|
8676
8970
|
startClosing() {
|
|
8677
8971
|
return new Promise(resolve => {
|
|
8678
|
-
setTimeout(() => {
|
|
8972
|
+
setTimeout(async () => {
|
|
8679
8973
|
this.isClosing = true;
|
|
8680
8974
|
// @HostBinding('class') updates are flushed when the host view is checked
|
|
8681
8975
|
// (appRef.tick), not by a bare detectChanges() on this dynamically-created
|
|
8682
|
-
// root component.
|
|
8683
|
-
//
|
|
8684
|
-
// fragile elsewhere. Apply it directly so the close animation is reliable.
|
|
8976
|
+
// root component. Apply the `.closing` class directly so the backdrop fade is
|
|
8977
|
+
// reliable in a zoneless app.
|
|
8685
8978
|
this.el.nativeElement.classList.add('closing');
|
|
8686
8979
|
this.cdr.detectChanges();
|
|
8687
8980
|
if (this.prefersReducedMotion()) {
|
|
8688
8981
|
resolve();
|
|
8689
8982
|
return;
|
|
8690
8983
|
}
|
|
8984
|
+
// Mobile: the bottom sheet drives the slide-down exit.
|
|
8985
|
+
const sheet = this.bottomSheet();
|
|
8986
|
+
if (sheet) {
|
|
8987
|
+
await sheet.startClosing();
|
|
8988
|
+
resolve();
|
|
8989
|
+
return;
|
|
8990
|
+
}
|
|
8991
|
+
// Desktop: wait for the centered dialog container's own close animation.
|
|
8691
8992
|
const container = this.el.nativeElement.querySelector('.modal-container');
|
|
8692
8993
|
if (!container) {
|
|
8693
8994
|
resolve();
|
|
@@ -8706,11 +9007,8 @@ class MnModalShellComponent {
|
|
|
8706
9007
|
clearTimeout(fallback);
|
|
8707
9008
|
resolve();
|
|
8708
9009
|
};
|
|
8709
|
-
// Normal close ends via a keyframe (animationend); the swipe-dismiss
|
|
8710
|
-
// glide ends via the transform transition (transitionend).
|
|
8711
9010
|
container.addEventListener('animationend', done);
|
|
8712
9011
|
container.addEventListener('transitionend', done);
|
|
8713
|
-
// `done` only runs asynchronously, after this assignment completes.
|
|
8714
9012
|
const fallback = setTimeout(done, MnModalShellComponent.CLOSE_FALLBACK_MS);
|
|
8715
9013
|
});
|
|
8716
9014
|
});
|
|
@@ -8738,123 +9036,54 @@ class MnModalShellComponent {
|
|
|
8738
9036
|
onCloseButtonClick() {
|
|
8739
9037
|
this.handleClose(ModalCloseReason.DISMISSED);
|
|
8740
9038
|
}
|
|
8741
|
-
/**
|
|
8742
|
-
*
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
? this.config.animation
|
|
8763
|
-
: this.config.animation?.type || 'slide';
|
|
8764
|
-
const animation = ` anim-${animType}`;
|
|
8765
|
-
const stacked = this.isStacked() ? ' is-stacked' : '';
|
|
8766
|
-
const mobileSheet = this.isMobileSheet ? ' mobile-sheet' : '';
|
|
8767
|
-
const swiping = this.swipeDismissing ? ' swipe-dismissing' : '';
|
|
8768
|
-
return `modal-shell modal-${size}${animation}${stacked}${mobileSheet}${swiping}`;
|
|
8769
|
-
}
|
|
8770
|
-
dragStartY = 0;
|
|
8771
|
-
/** Whether the sheet can be dismissed at all (drives whether the swipe is armed). */
|
|
8772
|
-
get canClose() {
|
|
8773
|
-
return this.config.closeMode !== CloseMode.DISABLED;
|
|
9039
|
+
/**
|
|
9040
|
+
* Guard consulted by the bottom sheet before it commits a swipe/flick/backdrop dismissal.
|
|
9041
|
+
* Mirrors the DISABLED/GUARDED rules of {@link handleClose} so a swipe cannot escape a
|
|
9042
|
+
* modal that a button close could not. Bound as a field so the template passes it directly.
|
|
9043
|
+
*/
|
|
9044
|
+
sheetDismissGuard = async () => {
|
|
9045
|
+
if (this.config.closeMode === CloseMode.DISABLED) {
|
|
9046
|
+
return false;
|
|
9047
|
+
}
|
|
9048
|
+
if (this.config.closeMode === CloseMode.GUARDED && this.config.closeGuard) {
|
|
9049
|
+
return await this.config.closeGuard();
|
|
9050
|
+
}
|
|
9051
|
+
return true;
|
|
9052
|
+
};
|
|
9053
|
+
/**
|
|
9054
|
+
* Handles the sheet's `(dismiss)` — emitted only after its guard passed and its exit
|
|
9055
|
+
* animation finished. Dismisses the modal (no re-guard) with a confirming haptic.
|
|
9056
|
+
*/
|
|
9057
|
+
onSheetDismiss() {
|
|
9058
|
+
this.haptics?.impact('medium');
|
|
9059
|
+
this.modalRef.dismiss(ModalCloseReason.DISMISSED);
|
|
8774
9060
|
}
|
|
8775
|
-
/** Tailwind's `sm` breakpoint — below this the modal renders as a bottom sheet. */
|
|
8776
|
-
static SHEET_MAX_WIDTH = 639.98;
|
|
8777
9061
|
ngAfterViewInit() {
|
|
8778
9062
|
this.previouslyFocusedElement = document.activeElement;
|
|
8779
9063
|
this.setupFocusTrap();
|
|
8780
|
-
// Focus the modal container
|
|
9064
|
+
// Focus the modal container (the centered dialog, or the sheet's container on mobile).
|
|
8781
9065
|
const container = this.el.nativeElement.querySelector('.modal-container');
|
|
8782
9066
|
if (container) {
|
|
8783
9067
|
container.focus();
|
|
8784
9068
|
}
|
|
8785
9069
|
}
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
if (window.innerWidth > MnModalShellComponent.SHEET_MAX_WIDTH)
|
|
8791
|
-
return;
|
|
8792
|
-
// Don't hijack drags that begin on an interactive control (e.g. the close button).
|
|
8793
|
-
if (event.target.closest('button'))
|
|
8794
|
-
return;
|
|
8795
|
-
this.isDraggingSheet = true;
|
|
8796
|
-
this.dragStartY = event.clientY;
|
|
8797
|
-
// Seed the velocity samples so a fast flick that releases on the first move still
|
|
8798
|
-
// has a baseline to measure against.
|
|
8799
|
-
this.lastSample = { y: event.clientY, t: event.timeStamp };
|
|
8800
|
-
this.prevSample = this.lastSample;
|
|
8801
|
-
event.target.setPointerCapture(event.pointerId);
|
|
8802
|
-
}
|
|
8803
|
-
onSheetPointerMove(event) {
|
|
8804
|
-
if (!this.isDraggingSheet)
|
|
8805
|
-
return;
|
|
8806
|
-
// Only track downward movement.
|
|
8807
|
-
this.sheetDragY = Math.max(0, event.clientY - this.dragStartY);
|
|
8808
|
-
// Roll the sample window forward so pointer-up can read the latest instantaneous speed.
|
|
8809
|
-
this.prevSample = this.lastSample;
|
|
8810
|
-
this.lastSample = { y: event.clientY, t: event.timeStamp };
|
|
8811
|
-
}
|
|
8812
|
-
async onSheetPointerUp() {
|
|
8813
|
-
if (!this.isDraggingSheet)
|
|
9070
|
+
/** Tracks the sheet breakpoint through `matchMedia` so the dialog/sheet fork re-renders
|
|
9071
|
+
* when the viewport crosses it. */
|
|
9072
|
+
startWatchingViewport() {
|
|
9073
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
|
|
8814
9074
|
return;
|
|
8815
|
-
this.
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
// A confirmed dismissal gets a slightly firmer tick than the open tap.
|
|
8820
|
-
this.haptics?.impact('medium');
|
|
8821
|
-
// Continue the gesture: glide the sheet the rest of the way down rather than
|
|
8822
|
-
// snapping back to 0 and replaying the slide-up keyframe (which looked un-animated).
|
|
8823
|
-
this.swipeDismissing = true;
|
|
8824
|
-
this.sheetDragY = window.innerHeight;
|
|
8825
|
-
this.cdr.detectChanges();
|
|
8826
|
-
}
|
|
8827
|
-
else {
|
|
8828
|
-
this.snapBack(); // guard rejected — spring back
|
|
8829
|
-
}
|
|
8830
|
-
}
|
|
8831
|
-
else {
|
|
8832
|
-
this.snapBack(); // not far enough / not a flick — spring back
|
|
8833
|
-
}
|
|
8834
|
-
this.lastSample = null;
|
|
8835
|
-
this.prevSample = null;
|
|
9075
|
+
this.sheetMedia = window.matchMedia(`(max-width: ${MnModalShellComponent.SHEET_MAX_WIDTH}px)`);
|
|
9076
|
+
this.isNarrow.set(this.sheetMedia.matches);
|
|
9077
|
+
this.sheetMediaListener = (event) => this.isNarrow.set(event.matches);
|
|
9078
|
+
this.sheetMedia.addEventListener('change', this.sheetMediaListener);
|
|
8836
9079
|
}
|
|
8837
|
-
/**
|
|
8838
|
-
|
|
8839
|
-
if (this.
|
|
8840
|
-
|
|
9080
|
+
/** Tears down the breakpoint listener. Idempotent. */
|
|
9081
|
+
stopWatchingViewport() {
|
|
9082
|
+
if (this.sheetMedia && this.sheetMediaListener) {
|
|
9083
|
+
this.sheetMedia.removeEventListener('change', this.sheetMediaListener);
|
|
8841
9084
|
}
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
}
|
|
8845
|
-
/** Downward release speed (px/ms) from the last two pointer samples. Positive means
|
|
8846
|
-
* moving down. Returns 0 when there is no usable sample window. */
|
|
8847
|
-
releaseVelocity() {
|
|
8848
|
-
if (!this.lastSample || !this.prevSample)
|
|
8849
|
-
return 0;
|
|
8850
|
-
const dt = this.lastSample.t - this.prevSample.t;
|
|
8851
|
-
if (dt <= 0)
|
|
8852
|
-
return 0;
|
|
8853
|
-
return (this.lastSample.y - this.prevSample.y) / dt;
|
|
8854
|
-
}
|
|
8855
|
-
/** Springs the sheet back to its resting position after a drag that didn't dismiss. */
|
|
8856
|
-
snapBack() {
|
|
8857
|
-
this.sheetDragY = 0;
|
|
9085
|
+
this.sheetMedia = null;
|
|
9086
|
+
this.sheetMediaListener = null;
|
|
8858
9087
|
}
|
|
8859
9088
|
/** Attempts to dismiss the modal. Resolves true if it was actually dismissed,
|
|
8860
9089
|
* false if blocked by a DISABLED close mode or a rejected close guard. */
|
|
@@ -8984,7 +9213,7 @@ class MnModalShellComponent {
|
|
|
8984
9213
|
}
|
|
8985
9214
|
}
|
|
8986
9215
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8987
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, viewQueries: [{ propertyName: "wizardBody", first: true, predicate: MnWizardBodyComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n [attr.aria-label]=\"closeModalLabel\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
|
|
9216
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, viewQueries: [{ propertyName: "wizardBody", first: true, predicate: MnWizardBodyComponent, descendants: true, isSignal: true }, { propertyName: "bottomSheet", first: true, predicate: MnBottomSheet, descendants: true, isSignal: true }], ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n@if (showMobileSheet) {\n <!-- Mobile: the shared bottom sheet owns the anchoring, grabber, swipe/flick-to-dismiss\n and slide animation. The shell keeps its own backdrop (so the sheet renders none),\n and its close animation delegates to the sheet's startClosing(). `modal-container`\n is kept as a class hook so the host app's keyboard-lift CSS still matches. -->\n <mn-bottom-sheet\n (dismiss)=\"onSheetDismiss()\"\n [ariaLabelledby]=\"config.title ? 'mn-modal-title' : undefined\"\n [containerClass]=\"'modal-container'\"\n [dismissGuard]=\"sheetDismissGuard\"\n [dismissible]=\"canClose\"\n [growWithKeyboard]=\"true\"\n [maxHeightVh]=\"92\"\n [showBackdrop]=\"false\"\n >\n <ng-container [ngTemplateOutlet]=\"modalBody\"></ng-container>\n </mn-bottom-sheet>\n} @else {\n <!--\n Desktop centered dialog. The container deliberately does NOT stop click propagation.\n The backdrop is a *sibling* element (absolutely positioned behind this one), never an\n ancestor, so clicks in here can't reach `onBackdropClick()` anyway. Swallowing them\n instead broke every \"click outside me\" handler inside a modal.\n -->\n <div\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n aria-modal=\"true\"\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n role=\"dialog\"\n tabindex=\"-1\"\n >\n <ng-container [ngTemplateOutlet]=\"modalBody\"></ng-container>\n </div>\n}\n\n<ng-template #modalBody>\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"showMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n [attr.aria-label]=\"closeModalLabel\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</ng-template>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.mobile-sheet){align-items:flex-end}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
|
|
8988
9217
|
}
|
|
8989
9218
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
8990
9219
|
type: Component,
|
|
@@ -8996,18 +9225,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
8996
9225
|
MnCustomBodyHostComponent,
|
|
8997
9226
|
MnFooterActionsComponent,
|
|
8998
9227
|
MnButton,
|
|
9228
|
+
MnBottomSheet,
|
|
8999
9229
|
LucideX,
|
|
9000
|
-
], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n
|
|
9230
|
+
], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n@if (showMobileSheet) {\n <!-- Mobile: the shared bottom sheet owns the anchoring, grabber, swipe/flick-to-dismiss\n and slide animation. The shell keeps its own backdrop (so the sheet renders none),\n and its close animation delegates to the sheet's startClosing(). `modal-container`\n is kept as a class hook so the host app's keyboard-lift CSS still matches. -->\n <mn-bottom-sheet\n (dismiss)=\"onSheetDismiss()\"\n [ariaLabelledby]=\"config.title ? 'mn-modal-title' : undefined\"\n [containerClass]=\"'modal-container'\"\n [dismissGuard]=\"sheetDismissGuard\"\n [dismissible]=\"canClose\"\n [growWithKeyboard]=\"true\"\n [maxHeightVh]=\"92\"\n [showBackdrop]=\"false\"\n >\n <ng-container [ngTemplateOutlet]=\"modalBody\"></ng-container>\n </mn-bottom-sheet>\n} @else {\n <!--\n Desktop centered dialog. The container deliberately does NOT stop click propagation.\n The backdrop is a *sibling* element (absolutely positioned behind this one), never an\n ancestor, so clicks in here can't reach `onBackdropClick()` anyway. Swallowing them\n instead broke every \"click outside me\" handler inside a modal.\n -->\n <div\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n aria-modal=\"true\"\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n role=\"dialog\"\n tabindex=\"-1\"\n >\n <ng-container [ngTemplateOutlet]=\"modalBody\"></ng-container>\n </div>\n}\n\n<ng-template #modalBody>\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"showMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n [attr.aria-label]=\"closeModalLabel\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</ng-template>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.mobile-sheet){align-items:flex-end}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"] }]
|
|
9001
9231
|
}], propDecorators: { config: [{
|
|
9002
9232
|
type: Input
|
|
9003
9233
|
}], modalRef: [{
|
|
9004
9234
|
type: Input
|
|
9005
|
-
}], wizardBody: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MnWizardBodyComponent), { isSignal: true }] }],
|
|
9006
|
-
type: HostListener,
|
|
9007
|
-
args: ['document:keydown.escape', ['$event']]
|
|
9008
|
-
}], hostClasses: [{
|
|
9235
|
+
}], wizardBody: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MnWizardBodyComponent), { isSignal: true }] }], bottomSheet: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MnBottomSheet), { isSignal: true }] }], hostClasses: [{
|
|
9009
9236
|
type: HostBinding,
|
|
9010
9237
|
args: ['class']
|
|
9238
|
+
}], onEscapeKey: [{
|
|
9239
|
+
type: HostListener,
|
|
9240
|
+
args: ['document:keydown.escape', ['$event']]
|
|
9011
9241
|
}] } });
|
|
9012
9242
|
|
|
9013
9243
|
class MnModalService {
|
|
@@ -11839,5 +12069,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
|
11839
12069
|
* Generated bundle index. Do not edit.
|
|
11840
12070
|
*/
|
|
11841
12071
|
|
|
11842
|
-
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnRichTextEditor, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultFilterPredicate, defaultIconForStyle, defaultTextAdapter, emptyFilterValue, enableMnPreviewMode, isFilterValueActive, isTranslatable, matchesColumnFilter, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig, resolveFilterableValue };
|
|
12072
|
+
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnBottomSheet, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnRichTextEditor, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultFilterPredicate, defaultIconForStyle, defaultTextAdapter, emptyFilterValue, enableMnPreviewMode, isFilterValueActive, isTranslatable, matchesColumnFilter, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig, resolveFilterableValue };
|
|
11843
12073
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|