mn-angular-lib 1.0.139 → 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 +568 -184
- 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 +6 -0
- package/types/mn-angular-lib.d.ts +281 -61
|
@@ -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,8 +3772,30 @@ 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;
|
|
3780
|
+
/** Option count at which the search input auto-enables when `searchable` is unset. */
|
|
3781
|
+
static DEFAULT_SEARCH_THRESHOLD = 8;
|
|
3782
|
+
/** Tailwind's `sm` breakpoint — below this the panel renders as a bottom sheet.
|
|
3783
|
+
* Kept in step with the same constant in `MnModalShellComponent`. */
|
|
3784
|
+
static SHEET_MAX_WIDTH = 639.98;
|
|
3785
|
+
/** Whether the viewport is currently narrow enough for the sheet layout. */
|
|
3786
|
+
isNarrowViewport = false;
|
|
3787
|
+
/** Live breakpoint match, so rotating the device re-evaluates the layout. */
|
|
3788
|
+
sheetMedia = null;
|
|
3789
|
+
/** The listener registered on `sheetMedia`, retained for teardown. */
|
|
3790
|
+
sheetMediaListener = null;
|
|
3791
|
+
/** `document.body`'s inline `overflow` before the sheet locked it, restored on close. */
|
|
3792
|
+
previousBodyOverflow = null;
|
|
3793
|
+
/**
|
|
3794
|
+
* The sheet's height (px) captured the moment it opened, before any search. Re-applied
|
|
3795
|
+
* as a `min-height` floor so filtering the option list shorter cannot shrink the sheet
|
|
3796
|
+
* mid-type. Null while anchored or closed, so the popover and desktop path are untouched.
|
|
3797
|
+
*/
|
|
3798
|
+
sheetFloorPx = null;
|
|
3511
3799
|
/**
|
|
3512
3800
|
* Watches the trigger while the panel is open. The panel lives in `document.body`,
|
|
3513
3801
|
* so it survives its own trigger being hidden by an ancestor — e.g. a wizard step
|
|
@@ -3521,6 +3809,8 @@ class MnMultiSelect {
|
|
|
3521
3809
|
* card) used to leave the portalled panel floating at its stale coordinates.
|
|
3522
3810
|
*/
|
|
3523
3811
|
scrollCapture = null;
|
|
3812
|
+
/** The bottom-sheet host currently moved into `document.body`, if any. */
|
|
3813
|
+
movedSheet = null;
|
|
3524
3814
|
/**
|
|
3525
3815
|
* The dropdown panel element, queried while it is rendered by the `@if` block.
|
|
3526
3816
|
* The setter relocates the panel to `document.body` so that its `position: fixed`
|
|
@@ -3530,7 +3820,7 @@ class MnMultiSelect {
|
|
|
3530
3820
|
* broken on iOS). Cleanup is handled when the query clears on close/destroy.
|
|
3531
3821
|
*/
|
|
3532
3822
|
set dropdownRef(ref) {
|
|
3533
|
-
this.
|
|
3823
|
+
this.movedPanel = this.portal(ref?.nativeElement ?? null, this.movedPanel);
|
|
3534
3824
|
}
|
|
3535
3825
|
/** Currently selected values */
|
|
3536
3826
|
selectedValues = [];
|
|
@@ -3549,28 +3839,65 @@ class MnMultiSelect {
|
|
|
3549
3839
|
if (this.ngControl)
|
|
3550
3840
|
this.ngControl.valueAccessor = this;
|
|
3551
3841
|
}
|
|
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
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Tracks the sheet breakpoint through `matchMedia` rather than reading `innerWidth`
|
|
3860
|
+
* once, so rotating the device switches layout instead of leaving a panel positioned
|
|
3861
|
+
* for the previous orientation. An open panel is closed on the switch — its anchored
|
|
3862
|
+
* coordinates and its sheet layout are not interchangeable.
|
|
3863
|
+
*/
|
|
3864
|
+
startWatchingViewport() {
|
|
3865
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
|
|
3866
|
+
return;
|
|
3867
|
+
this.sheetMedia = window.matchMedia(`(max-width: ${MnMultiSelect.SHEET_MAX_WIDTH}px)`);
|
|
3868
|
+
this.isNarrowViewport = this.sheetMedia.matches;
|
|
3869
|
+
this.sheetMediaListener = (event) => {
|
|
3870
|
+
this.isNarrowViewport = event.matches;
|
|
3871
|
+
this.close();
|
|
3872
|
+
// The listener fires outside Angular, so a zoneless app needs an explicit nudge.
|
|
3873
|
+
this.cdr.markForCheck();
|
|
3874
|
+
};
|
|
3875
|
+
this.sheetMedia.addEventListener('change', this.sheetMediaListener);
|
|
3876
|
+
}
|
|
3877
|
+
/** Tears down the breakpoint listener. Idempotent. */
|
|
3878
|
+
stopWatchingViewport() {
|
|
3879
|
+
if (this.sheetMedia && this.sheetMediaListener) {
|
|
3880
|
+
this.sheetMedia.removeEventListener('change', this.sheetMediaListener);
|
|
3881
|
+
}
|
|
3882
|
+
this.sheetMedia = null;
|
|
3883
|
+
this.sheetMediaListener = null;
|
|
3884
|
+
}
|
|
3552
3885
|
ngOnInit() {
|
|
3553
3886
|
this.resolveConfig();
|
|
3887
|
+
this.startWatchingViewport();
|
|
3554
3888
|
const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
|
|
3555
3889
|
this.resolveConfig();
|
|
3556
3890
|
});
|
|
3557
3891
|
this.destroyRef.onDestroy(() => {
|
|
3558
3892
|
sub.unsubscribe();
|
|
3559
3893
|
this.stopWatchingTrigger();
|
|
3560
|
-
|
|
3561
|
-
this.
|
|
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);
|
|
3562
3899
|
});
|
|
3563
3900
|
}
|
|
3564
|
-
onDocumentClick(event) {
|
|
3565
|
-
const target = event.target;
|
|
3566
|
-
// The panel lives at the body root once open, so it is not a descendant of the
|
|
3567
|
-
// host element — treat clicks inside the portalled panel as "inside" too.
|
|
3568
|
-
const insideHost = !!target && this.elRef.nativeElement.contains(target);
|
|
3569
|
-
const insidePanel = !!target && !!this.movedPanel && this.movedPanel.contains(target);
|
|
3570
|
-
if (!insideHost && !insidePanel) {
|
|
3571
|
-
this.close();
|
|
3572
|
-
}
|
|
3573
|
-
}
|
|
3574
3901
|
resolveConfig() {
|
|
3575
3902
|
const instanceId = this.explicitInstanceId || `mn-multi-select-${this.props.id}`;
|
|
3576
3903
|
this.uiConfig = this.configService.resolve('mn-multi-select', this.sectionPath, instanceId);
|
|
@@ -3603,15 +3930,84 @@ class MnMultiSelect {
|
|
|
3603
3930
|
return;
|
|
3604
3931
|
}
|
|
3605
3932
|
this.isOpen = true;
|
|
3933
|
+
if (this.isSheet) {
|
|
3934
|
+
// A sheet is anchored to the viewport, so it needs no trigger tracking — only a
|
|
3935
|
+
// scroll lock so the page behind it stays put while the list is scrolled.
|
|
3936
|
+
this.lockBodyScroll();
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3606
3939
|
this.updateDropdownPosition();
|
|
3607
3940
|
this.startWatchingTrigger();
|
|
3608
3941
|
}
|
|
3942
|
+
/** Whether the panel should currently render as a bottom sheet. */
|
|
3943
|
+
get isSheet() {
|
|
3944
|
+
return this.props.mobileSheet !== false && this.isNarrowViewport;
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* Whether the search input is shown: the explicit `searchable` prop when set,
|
|
3948
|
+
* otherwise auto-enabled once the option count reaches the threshold.
|
|
3949
|
+
*/
|
|
3950
|
+
get isSearchable() {
|
|
3951
|
+
if (this.props.searchable !== undefined)
|
|
3952
|
+
return this.props.searchable;
|
|
3953
|
+
const threshold = this.props.searchThreshold ?? MnMultiSelect.DEFAULT_SEARCH_THRESHOLD;
|
|
3954
|
+
return this.props.options.length >= threshold;
|
|
3955
|
+
}
|
|
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
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
/**
|
|
3970
|
+
* Records the sheet's opened height as its `min-height` floor. Measured on the next
|
|
3971
|
+
* frame so the read reflects the fully-rendered, unfiltered list (the search box is
|
|
3972
|
+
* empty on open) and never forces a reflow mid change-detection. The floor equals the
|
|
3973
|
+
* content height at that instant, so applying it triggers no resize — it only stops a
|
|
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.
|
|
3978
|
+
*/
|
|
3979
|
+
captureSheetFloor(hostEl) {
|
|
3980
|
+
const measure = () => {
|
|
3981
|
+
const container = hostEl.querySelector('.mn-sheet-container');
|
|
3982
|
+
return container?.offsetHeight ?? hostEl.offsetHeight;
|
|
3983
|
+
};
|
|
3984
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
3985
|
+
this.sheetFloorPx = measure();
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
requestAnimationFrame(() => {
|
|
3989
|
+
// The sheet may have closed before the frame ran; don't strand a stale floor.
|
|
3990
|
+
if (!this.isOpen || this.movedSheet !== hostEl)
|
|
3991
|
+
return;
|
|
3992
|
+
this.sheetFloorPx = measure();
|
|
3993
|
+
this.cdr.markForCheck();
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3609
3996
|
/** Closes the dropdown on Escape for keyboard accessibility. */
|
|
3610
3997
|
onEscape() {
|
|
3611
3998
|
this.close();
|
|
3612
3999
|
}
|
|
3613
|
-
/**
|
|
4000
|
+
/**
|
|
4001
|
+
* Closes the dropdown when the page or a scrollable parent is scrolled.
|
|
4002
|
+
*
|
|
4003
|
+
* Skipped for a sheet: it is anchored to the viewport, not to the trigger, so it has
|
|
4004
|
+
* no stale position to escape. Crucially, opening the soft keyboard fires a `resize`
|
|
4005
|
+
* on Android — closing on that would dismiss the sheet the instant search is focused.
|
|
4006
|
+
* A genuine layout switch is handled by the `matchMedia` listener instead.
|
|
4007
|
+
*/
|
|
3614
4008
|
onWindowScrollOrResize() {
|
|
4009
|
+
if (this.isSheet)
|
|
4010
|
+
return;
|
|
3615
4011
|
this.close();
|
|
3616
4012
|
}
|
|
3617
4013
|
/**
|
|
@@ -3625,6 +4021,29 @@ class MnMultiSelect {
|
|
|
3625
4021
|
this.isOpen = false;
|
|
3626
4022
|
this.searchTerm = '';
|
|
3627
4023
|
this.stopWatchingTrigger();
|
|
4024
|
+
this.unlockBodyScroll();
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* Freezes the page behind an open sheet. The previous inline value is captured and
|
|
4028
|
+
* restored verbatim so a surrounding modal that set its own lock is left intact.
|
|
4029
|
+
*/
|
|
4030
|
+
lockBodyScroll() {
|
|
4031
|
+
if (this.previousBodyOverflow !== null)
|
|
4032
|
+
return;
|
|
4033
|
+
this.previousBodyOverflow = document.body.style.overflow;
|
|
4034
|
+
this.renderer.setStyle(document.body, 'overflow', 'hidden');
|
|
4035
|
+
}
|
|
4036
|
+
/** Restores the pre-lock `overflow`. Idempotent. */
|
|
4037
|
+
unlockBodyScroll() {
|
|
4038
|
+
if (this.previousBodyOverflow === null)
|
|
4039
|
+
return;
|
|
4040
|
+
if (this.previousBodyOverflow) {
|
|
4041
|
+
this.renderer.setStyle(document.body, 'overflow', this.previousBodyOverflow);
|
|
4042
|
+
}
|
|
4043
|
+
else {
|
|
4044
|
+
this.renderer.removeStyle(document.body, 'overflow');
|
|
4045
|
+
}
|
|
4046
|
+
this.previousBodyOverflow = null;
|
|
3628
4047
|
}
|
|
3629
4048
|
/** Calculates the fixed position for the dropdown based on the trigger element */
|
|
3630
4049
|
updateDropdownPosition() {
|
|
@@ -3667,27 +4086,29 @@ class MnMultiSelect {
|
|
|
3667
4086
|
document.addEventListener('scroll', this.scrollCapture, true);
|
|
3668
4087
|
}
|
|
3669
4088
|
/**
|
|
3670
|
-
* Move
|
|
3671
|
-
*
|
|
3672
|
-
* `transform`/`filter`/`will-change`, so `position: fixed` anchors to the viewport
|
|
3673
|
-
*
|
|
4089
|
+
* Move an overlay element to `document.body` when it appears, and detach it when the
|
|
4090
|
+
* query clears. Appending to the body root makes the element immune to ancestor
|
|
4091
|
+
* `transform`/`filter`/`will-change`, so `position: fixed` anchors to the viewport —
|
|
4092
|
+
* without this the panel lands mid-screen (and breaks outright on iOS).
|
|
4093
|
+
*
|
|
4094
|
+
* Returns the element now portalled, so the caller can store it. Idempotent and safe
|
|
4095
|
+
* to call with `null`.
|
|
3674
4096
|
*/
|
|
3675
|
-
|
|
4097
|
+
portal(el, current) {
|
|
3676
4098
|
if (el) {
|
|
3677
|
-
if (
|
|
3678
|
-
return;
|
|
4099
|
+
if (current === el)
|
|
4100
|
+
return current;
|
|
3679
4101
|
this.renderer.appendChild(document.body, el);
|
|
3680
|
-
|
|
3681
|
-
return;
|
|
4102
|
+
return el;
|
|
3682
4103
|
}
|
|
3683
|
-
if (
|
|
4104
|
+
if (current) {
|
|
3684
4105
|
// Angular's view teardown may already have removed it; only detach if still attached.
|
|
3685
|
-
const parent =
|
|
4106
|
+
const parent = current.parentNode;
|
|
3686
4107
|
if (parent) {
|
|
3687
|
-
this.renderer.removeChild(parent,
|
|
4108
|
+
this.renderer.removeChild(parent, current);
|
|
3688
4109
|
}
|
|
3689
|
-
this.movedPanel = null;
|
|
3690
4110
|
}
|
|
4111
|
+
return null;
|
|
3691
4112
|
}
|
|
3692
4113
|
/** Tears down the watchers installed by `startWatchingTrigger`. Idempotent. */
|
|
3693
4114
|
stopWatchingTrigger() {
|
|
@@ -3843,11 +4264,11 @@ class MnMultiSelect {
|
|
|
3843
4264
|
});
|
|
3844
4265
|
}
|
|
3845
4266
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3846
|
-
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 }], 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 <!-- 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 class=\"fixed z-9999 bg-base-100 border border-base-300 rounded-md shadow-lg max-h-60 overflow-auto\"\n [style.top]=\"dropdownStyle.top\"\n [style.left]=\"dropdownStyle.left\"\n [style.width]=\"dropdownStyle.width\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (props.searchable) {\n <div class=\"p-2 border-b border-base-300\">\n <input\n type=\"text\"\n class=\"w-full p-1.5 text-sm border border-base-300 rounded-md outline-none focus:border-primary-500 bg-base-200 text-base-content placeholder-base-content/50\"\n [placeholder]=\"props.searchPlaceholder || 'Search...'\"\n [value]=\"searchTerm\"\n (input)=\"onSearch(($any($event.target)).value)\"\n (click)=\"$event.stopPropagation()\"\n />\n </div>\n }\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 px-3 py-2 text-sm cursor-pointer text-base-content hover:bg-base-200\"\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\"\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=\"px-3 py-2 text-sm text-base-content/50\">{{ uiConfig.noOptionsFound || 'No options found' }}</div>\n }\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", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: 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]" }] });
|
|
3847
4268
|
}
|
|
3848
4269
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, decorators: [{
|
|
3849
4270
|
type: Component,
|
|
3850
|
-
args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, MnErrorMessage, MnButton, 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" }]
|
|
3851
4272
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
3852
4273
|
type: Input,
|
|
3853
4274
|
args: [{ required: true }]
|
|
@@ -3857,6 +4278,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3857
4278
|
}], dropdownRef: [{
|
|
3858
4279
|
type: ViewChild,
|
|
3859
4280
|
args: ['dropdown', { static: false }]
|
|
4281
|
+
}], sheetRef: [{
|
|
4282
|
+
type: ViewChild,
|
|
4283
|
+
args: ['sheet', { static: false, read: ElementRef }]
|
|
3860
4284
|
}], onDocumentClick: [{
|
|
3861
4285
|
type: HostListener,
|
|
3862
4286
|
args: ['document:click', ['$event']]
|
|
@@ -8412,9 +8836,6 @@ class MnModalShellComponent {
|
|
|
8412
8836
|
}
|
|
8413
8837
|
el = inject(ElementRef);
|
|
8414
8838
|
cdr = inject(ChangeDetectorRef);
|
|
8415
|
-
/** Downward release speed (px/ms) above which a short drag still dismisses — a "flick".
|
|
8416
|
-
* Native sheets dismiss on a quick flick regardless of distance, not just a long drag. */
|
|
8417
|
-
static FLICK_VELOCITY = 0.5;
|
|
8418
8839
|
config;
|
|
8419
8840
|
modalRef;
|
|
8420
8841
|
isClosing = false;
|
|
@@ -8429,6 +8850,8 @@ class MnModalShellComponent {
|
|
|
8429
8850
|
ModalKind = ModalKind;
|
|
8430
8851
|
/** The rendered wizard body, when this modal is a wizard — used to read the active step title. */
|
|
8431
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;
|
|
8432
8855
|
/**
|
|
8433
8856
|
* Title of the wizard's current step, or undefined for non-wizard modals.
|
|
8434
8857
|
* The template appends it to the modal title on small screens, where the
|
|
@@ -8439,19 +8862,42 @@ class MnModalShellComponent {
|
|
|
8439
8862
|
focusTrapListener = null;
|
|
8440
8863
|
pollingTimer = null;
|
|
8441
8864
|
pollAttempts = 0;
|
|
8442
|
-
|
|
8443
|
-
|
|
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;
|
|
8444
8881
|
}
|
|
8445
|
-
/**
|
|
8446
|
-
*
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
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}`;
|
|
8455
8901
|
}
|
|
8456
8902
|
setupFocusTrap() {
|
|
8457
8903
|
this.focusTrapListener = (e) => {
|
|
@@ -8495,45 +8941,54 @@ class MnModalShellComponent {
|
|
|
8495
8941
|
asCustom(config) {
|
|
8496
8942
|
return config;
|
|
8497
8943
|
}
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
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
|
+
}
|
|
8511
8960
|
}
|
|
8512
8961
|
/**
|
|
8513
8962
|
* Triggers the closing animation and resolves once it has actually finished.
|
|
8514
8963
|
*
|
|
8515
|
-
* Deferred via setTimeout to avoid NG0100 when called during a CD cycle.
|
|
8516
|
-
*
|
|
8517
|
-
*
|
|
8518
|
-
*
|
|
8519
|
-
*
|
|
8520
|
-
* 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).
|
|
8521
8969
|
*/
|
|
8522
8970
|
startClosing() {
|
|
8523
8971
|
return new Promise(resolve => {
|
|
8524
|
-
setTimeout(() => {
|
|
8972
|
+
setTimeout(async () => {
|
|
8525
8973
|
this.isClosing = true;
|
|
8526
8974
|
// @HostBinding('class') updates are flushed when the host view is checked
|
|
8527
8975
|
// (appRef.tick), not by a bare detectChanges() on this dynamically-created
|
|
8528
|
-
// root component.
|
|
8529
|
-
//
|
|
8530
|
-
// 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.
|
|
8531
8978
|
this.el.nativeElement.classList.add('closing');
|
|
8532
8979
|
this.cdr.detectChanges();
|
|
8533
8980
|
if (this.prefersReducedMotion()) {
|
|
8534
8981
|
resolve();
|
|
8535
8982
|
return;
|
|
8536
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.
|
|
8537
8992
|
const container = this.el.nativeElement.querySelector('.modal-container');
|
|
8538
8993
|
if (!container) {
|
|
8539
8994
|
resolve();
|
|
@@ -8552,11 +9007,8 @@ class MnModalShellComponent {
|
|
|
8552
9007
|
clearTimeout(fallback);
|
|
8553
9008
|
resolve();
|
|
8554
9009
|
};
|
|
8555
|
-
// Normal close ends via a keyframe (animationend); the swipe-dismiss
|
|
8556
|
-
// glide ends via the transform transition (transitionend).
|
|
8557
9010
|
container.addEventListener('animationend', done);
|
|
8558
9011
|
container.addEventListener('transitionend', done);
|
|
8559
|
-
// `done` only runs asynchronously, after this assignment completes.
|
|
8560
9012
|
const fallback = setTimeout(done, MnModalShellComponent.CLOSE_FALLBACK_MS);
|
|
8561
9013
|
});
|
|
8562
9014
|
});
|
|
@@ -8584,123 +9036,54 @@ class MnModalShellComponent {
|
|
|
8584
9036
|
onCloseButtonClick() {
|
|
8585
9037
|
this.handleClose(ModalCloseReason.DISMISSED);
|
|
8586
9038
|
}
|
|
8587
|
-
/**
|
|
8588
|
-
*
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
? this.config.animation
|
|
8609
|
-
: this.config.animation?.type || 'slide';
|
|
8610
|
-
const animation = ` anim-${animType}`;
|
|
8611
|
-
const stacked = this.isStacked() ? ' is-stacked' : '';
|
|
8612
|
-
const mobileSheet = this.isMobileSheet ? ' mobile-sheet' : '';
|
|
8613
|
-
const swiping = this.swipeDismissing ? ' swipe-dismissing' : '';
|
|
8614
|
-
return `modal-shell modal-${size}${animation}${stacked}${mobileSheet}${swiping}`;
|
|
8615
|
-
}
|
|
8616
|
-
dragStartY = 0;
|
|
8617
|
-
/** Whether the sheet can be dismissed at all (drives whether the swipe is armed). */
|
|
8618
|
-
get canClose() {
|
|
8619
|
-
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);
|
|
8620
9060
|
}
|
|
8621
|
-
/** Tailwind's `sm` breakpoint — below this the modal renders as a bottom sheet. */
|
|
8622
|
-
static SHEET_MAX_WIDTH = 639.98;
|
|
8623
9061
|
ngAfterViewInit() {
|
|
8624
9062
|
this.previouslyFocusedElement = document.activeElement;
|
|
8625
9063
|
this.setupFocusTrap();
|
|
8626
|
-
// Focus the modal container
|
|
9064
|
+
// Focus the modal container (the centered dialog, or the sheet's container on mobile).
|
|
8627
9065
|
const container = this.el.nativeElement.querySelector('.modal-container');
|
|
8628
9066
|
if (container) {
|
|
8629
9067
|
container.focus();
|
|
8630
9068
|
}
|
|
8631
9069
|
}
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
if (window.innerWidth > MnModalShellComponent.SHEET_MAX_WIDTH)
|
|
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')
|
|
8637
9074
|
return;
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
this.
|
|
8642
|
-
this.dragStartY = event.clientY;
|
|
8643
|
-
// Seed the velocity samples so a fast flick that releases on the first move still
|
|
8644
|
-
// has a baseline to measure against.
|
|
8645
|
-
this.lastSample = { y: event.clientY, t: event.timeStamp };
|
|
8646
|
-
this.prevSample = this.lastSample;
|
|
8647
|
-
event.target.setPointerCapture(event.pointerId);
|
|
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);
|
|
8648
9079
|
}
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
this.sheetDragY = Math.max(0, event.clientY - this.dragStartY);
|
|
8654
|
-
// Roll the sample window forward so pointer-up can read the latest instantaneous speed.
|
|
8655
|
-
this.prevSample = this.lastSample;
|
|
8656
|
-
this.lastSample = { y: event.clientY, t: event.timeStamp };
|
|
8657
|
-
}
|
|
8658
|
-
async onSheetPointerUp() {
|
|
8659
|
-
if (!this.isDraggingSheet)
|
|
8660
|
-
return;
|
|
8661
|
-
this.isDraggingSheet = false;
|
|
8662
|
-
if (this.shouldDismissSheet()) {
|
|
8663
|
-
const closed = await this.handleClose(ModalCloseReason.DISMISSED);
|
|
8664
|
-
if (closed) {
|
|
8665
|
-
// A confirmed dismissal gets a slightly firmer tick than the open tap.
|
|
8666
|
-
this.haptics?.impact('medium');
|
|
8667
|
-
// Continue the gesture: glide the sheet the rest of the way down rather than
|
|
8668
|
-
// snapping back to 0 and replaying the slide-up keyframe (which looked un-animated).
|
|
8669
|
-
this.swipeDismissing = true;
|
|
8670
|
-
this.sheetDragY = window.innerHeight;
|
|
8671
|
-
this.cdr.detectChanges();
|
|
8672
|
-
}
|
|
8673
|
-
else {
|
|
8674
|
-
this.snapBack(); // guard rejected — spring back
|
|
8675
|
-
}
|
|
8676
|
-
}
|
|
8677
|
-
else {
|
|
8678
|
-
this.snapBack(); // not far enough / not a flick — spring back
|
|
9080
|
+
/** Tears down the breakpoint listener. Idempotent. */
|
|
9081
|
+
stopWatchingViewport() {
|
|
9082
|
+
if (this.sheetMedia && this.sheetMediaListener) {
|
|
9083
|
+
this.sheetMedia.removeEventListener('change', this.sheetMediaListener);
|
|
8679
9084
|
}
|
|
8680
|
-
this.
|
|
8681
|
-
this.
|
|
8682
|
-
}
|
|
8683
|
-
/** Whether the release should dismiss: a long-enough drag OR a fast downward flick. */
|
|
8684
|
-
shouldDismissSheet() {
|
|
8685
|
-
if (this.sheetDragY > MnModalShellComponent.SWIPE_DISMISS_THRESHOLD) {
|
|
8686
|
-
return true;
|
|
8687
|
-
}
|
|
8688
|
-
return this.releaseVelocity() > MnModalShellComponent.FLICK_VELOCITY
|
|
8689
|
-
&& this.sheetDragY > MnModalShellComponent.FLICK_MIN_DISTANCE;
|
|
8690
|
-
}
|
|
8691
|
-
/** Downward release speed (px/ms) from the last two pointer samples. Positive means
|
|
8692
|
-
* moving down. Returns 0 when there is no usable sample window. */
|
|
8693
|
-
releaseVelocity() {
|
|
8694
|
-
if (!this.lastSample || !this.prevSample)
|
|
8695
|
-
return 0;
|
|
8696
|
-
const dt = this.lastSample.t - this.prevSample.t;
|
|
8697
|
-
if (dt <= 0)
|
|
8698
|
-
return 0;
|
|
8699
|
-
return (this.lastSample.y - this.prevSample.y) / dt;
|
|
8700
|
-
}
|
|
8701
|
-
/** Springs the sheet back to its resting position after a drag that didn't dismiss. */
|
|
8702
|
-
snapBack() {
|
|
8703
|
-
this.sheetDragY = 0;
|
|
9085
|
+
this.sheetMedia = null;
|
|
9086
|
+
this.sheetMediaListener = null;
|
|
8704
9087
|
}
|
|
8705
9088
|
/** Attempts to dismiss the modal. Resolves true if it was actually dismissed,
|
|
8706
9089
|
* false if blocked by a DISABLED close mode or a rejected close guard. */
|
|
@@ -8830,7 +9213,7 @@ class MnModalShellComponent {
|
|
|
8830
9213
|
}
|
|
8831
9214
|
}
|
|
8832
9215
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8833
|
-
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]" }] });
|
|
8834
9217
|
}
|
|
8835
9218
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
8836
9219
|
type: Component,
|
|
@@ -8842,18 +9225,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
8842
9225
|
MnCustomBodyHostComponent,
|
|
8843
9226
|
MnFooterActionsComponent,
|
|
8844
9227
|
MnButton,
|
|
9228
|
+
MnBottomSheet,
|
|
8845
9229
|
LucideX,
|
|
8846
|
-
], 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"] }]
|
|
8847
9231
|
}], propDecorators: { config: [{
|
|
8848
9232
|
type: Input
|
|
8849
9233
|
}], modalRef: [{
|
|
8850
9234
|
type: Input
|
|
8851
|
-
}], wizardBody: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MnWizardBodyComponent), { isSignal: true }] }],
|
|
8852
|
-
type: HostListener,
|
|
8853
|
-
args: ['document:keydown.escape', ['$event']]
|
|
8854
|
-
}], hostClasses: [{
|
|
9235
|
+
}], wizardBody: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MnWizardBodyComponent), { isSignal: true }] }], bottomSheet: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MnBottomSheet), { isSignal: true }] }], hostClasses: [{
|
|
8855
9236
|
type: HostBinding,
|
|
8856
9237
|
args: ['class']
|
|
9238
|
+
}], onEscapeKey: [{
|
|
9239
|
+
type: HostListener,
|
|
9240
|
+
args: ['document:keydown.escape', ['$event']]
|
|
8857
9241
|
}] } });
|
|
8858
9242
|
|
|
8859
9243
|
class MnModalService {
|
|
@@ -11685,5 +12069,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
|
11685
12069
|
* Generated bundle index. Do not edit.
|
|
11686
12070
|
*/
|
|
11687
12071
|
|
|
11688
|
-
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 };
|
|
11689
12073
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|