mn-angular-lib 1.0.102 → 1.0.104
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 +321 -133
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mn-angular-lib.d.ts +143 -17
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, inject, HostBinding, Input, Component, ChangeDetectionStrategy, ApplicationRef, APP_INITIALIZER, Pipe, signal, DestroyRef, Optional, SkipSelf, Attribute, Directive, ElementRef, EventEmitter, computed, Output, HostListener, ViewChild, ViewContainerRef, forwardRef, Renderer2, ChangeDetectorRef, afterEveryRender, TemplateRef, ViewChildren, EnvironmentInjector, createComponent, isSignal } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, inject, HostBinding, Input, Component, ChangeDetectionStrategy, ApplicationRef, APP_INITIALIZER, Pipe, signal, DestroyRef, Optional, SkipSelf, Attribute, Directive, ElementRef, EventEmitter, computed, Output, input, output, HostListener, ViewChild, ViewContainerRef, forwardRef, Renderer2, ChangeDetectorRef, afterEveryRender, TemplateRef, ViewChildren, viewChild, EnvironmentInjector, createComponent, isSignal } from '@angular/core';
|
|
3
3
|
export { TemplateRef, Type } from '@angular/core';
|
|
4
|
-
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
|
|
4
|
+
import { BehaviorSubject, firstValueFrom, skip, fromEvent, Subject, debounceTime as debounceTime$1, of, takeUntil, map, catchError } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgClass, NgOptimizedImage, NgTemplateOutlet } from '@angular/common';
|
|
7
7
|
import { tv } from 'tailwind-variants';
|
|
@@ -9,7 +9,9 @@ import * as i2 from '@angular/forms';
|
|
|
9
9
|
import { NgControl, Validators, FormsModule, NG_VALUE_ACCESSOR, FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
10
10
|
import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
|
|
11
11
|
import JSON5 from 'json5';
|
|
12
|
-
import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideFilter, LucideFunnel } from '@lucide/angular';
|
|
12
|
+
import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideChevronLeft, LucideChevronRight, LucideFilter, LucideFunnel } from '@lucide/angular';
|
|
13
|
+
import { debounceTime } from 'rxjs/operators';
|
|
14
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
13
15
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
14
16
|
|
|
15
17
|
// projects/mn-angular-lib/src/lib/mn-mn-alert/mn-mn-alert.tokens.ts
|
|
@@ -2895,7 +2897,7 @@ class MnDatetime {
|
|
|
2895
2897
|
});
|
|
2896
2898
|
}
|
|
2897
2899
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDatetime, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2898
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDatetime, isStandalone: true, selector: "mn-lib-datetime", inputs: { props: "props" }, host: { properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, 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 <input\n #dateInput\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\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 [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (click)=\"handleClick(dateInput)\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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: ["input::-webkit-calendar-picker-indicator{cursor:pointer}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
2900
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDatetime, isStandalone: true, selector: "mn-lib-datetime", inputs: { props: "props" }, host: { properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, 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 <input\n #dateInput\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\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 [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (click)=\"handleClick(dateInput)\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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: ["input::-webkit-calendar-picker-indicator{cursor:pointer}@media(pointer:coarse){input[type=date],input[type=datetime-local],input[type=time],input[type=month],input[type=week]{-webkit-appearance:none;appearance:none;min-width:0;box-sizing:border-box}}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
2899
2901
|
}
|
|
2900
2902
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDatetime, decorators: [{
|
|
2901
2903
|
type: Component,
|
|
@@ -2908,12 +2910,227 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2908
2910
|
// when fullWidth is requested so 100% has something to fill.
|
|
2909
2911
|
'[style.display]': "props?.fullWidth ? 'block' : null",
|
|
2910
2912
|
'[style.width]': "props?.fullWidth ? '100%' : null",
|
|
2911
|
-
}, 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 <input\n #dateInput\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\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 [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (click)=\"handleClick(dateInput)\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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: ["input::-webkit-calendar-picker-indicator{cursor:pointer}\n"] }]
|
|
2913
|
+
}, 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 <input\n #dateInput\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\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 [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (click)=\"handleClick(dateInput)\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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: ["input::-webkit-calendar-picker-indicator{cursor:pointer}@media(pointer:coarse){input[type=date],input[type=datetime-local],input[type=time],input[type=month],input[type=week]{-webkit-appearance:none;appearance:none;min-width:0;box-sizing:border-box}}\n"] }]
|
|
2912
2914
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
2913
2915
|
type: Input,
|
|
2914
2916
|
args: [{ required: true }]
|
|
2915
2917
|
}] } });
|
|
2916
2918
|
|
|
2919
|
+
/**
|
|
2920
|
+
* Custom day-tile breakpoint ladder, evaluated from widest to narrowest so the
|
|
2921
|
+
* first matching {@link TileBreakpoint.minWidth} wins.
|
|
2922
|
+
*
|
|
2923
|
+
* The counts are tuned to the bar's two-layout responsive design. Below the
|
|
2924
|
+
* template's `xl` breakpoint (1280px) the bar stacks and the day strip gets its
|
|
2925
|
+
* own full-width row, so it can afford more tiles; at/above `xl` everything sits on
|
|
2926
|
+
* one row alongside the Today button, arrows, divider and date picker (≥1500px);
|
|
2927
|
+
* below that the strip sits on its own full-width row, so a full week fits without
|
|
2928
|
+
* crowding. The steps:
|
|
2929
|
+
* - ≥ 1028px: 7 tiles — the strip's own row (or the wide single-row layout) has
|
|
2930
|
+
* room for a full week.
|
|
2931
|
+
* - 640–1027px: 5 tiles — small tablets / large phones.
|
|
2932
|
+
* - below 640px: 3 tiles — phones stay compact and page via the arrows.
|
|
2933
|
+
*
|
|
2934
|
+
* Note: below {@link MnDateSelectorBar.MIN_STRIP_WIDTH}px the strip is hidden
|
|
2935
|
+
* entirely (only Today + the date picker remain), so those counts never render.
|
|
2936
|
+
*/
|
|
2937
|
+
const TILE_BREAKPOINTS = [
|
|
2938
|
+
{ minWidth: 1028, tiles: 7 },
|
|
2939
|
+
{ minWidth: 640, tiles: 5 },
|
|
2940
|
+
{ minWidth: 0, tiles: 3 },
|
|
2941
|
+
];
|
|
2942
|
+
/**
|
|
2943
|
+
* Reusable, responsive date selector bar.
|
|
2944
|
+
*
|
|
2945
|
+
* Renders a "Today" button, previous/next day-window arrows, a strip of day
|
|
2946
|
+
* tiles (3 → 5 → 7 as the viewport widens; see {@link TILE_BREAKPOINTS}) and a
|
|
2947
|
+
* date picker. Selecting a
|
|
2948
|
+
* tile, pressing "Today", or picking a date through the picker emits the chosen
|
|
2949
|
+
* day via {@link dateSelected}. The arrows only shift the visible tile window and
|
|
2950
|
+
* never emit.
|
|
2951
|
+
*
|
|
2952
|
+
* The component carries no hard-coded copy: button/placeholder text is supplied
|
|
2953
|
+
* through {@link todayLabel} / {@link pickDateLabel}, and day/month names follow
|
|
2954
|
+
* {@link locale} (falling back to the active {@link MnLanguageService} locale).
|
|
2955
|
+
*
|
|
2956
|
+
* @example
|
|
2957
|
+
* ```html
|
|
2958
|
+
* <mn-date-selector-bar
|
|
2959
|
+
* [selectedDate]="focusDay"
|
|
2960
|
+
* [todayLabel]="'Today'"
|
|
2961
|
+
* [pickDateLabel]="'Pick a date'"
|
|
2962
|
+
* [locale]="'nl-NL'"
|
|
2963
|
+
* (dateSelected)="onDaySelected($event)">
|
|
2964
|
+
* </mn-date-selector-bar>
|
|
2965
|
+
* ```
|
|
2966
|
+
*/
|
|
2967
|
+
class MnDateSelectorBar {
|
|
2968
|
+
/**
|
|
2969
|
+
* Minimum viewport width (px) at which the day strip (arrows + tiles) is shown.
|
|
2970
|
+
* On narrower screens there isn't room for it, so only the Today button and the
|
|
2971
|
+
* date picker remain.
|
|
2972
|
+
*/
|
|
2973
|
+
static MIN_STRIP_WIDTH = 320;
|
|
2974
|
+
/** The currently selected date, provided by the parent. */
|
|
2975
|
+
selectedDate = input(this.getToday(), ...(ngDevMode ? [{ debugName: "selectedDate" }] : []));
|
|
2976
|
+
/** Label for the "Today" button. */
|
|
2977
|
+
todayLabel = input('Today', ...(ngDevMode ? [{ debugName: "todayLabel" }] : []));
|
|
2978
|
+
/** Placeholder for the date picker input. */
|
|
2979
|
+
pickDateLabel = input('Pick a date', ...(ngDevMode ? [{ debugName: "pickDateLabel" }] : []));
|
|
2980
|
+
/**
|
|
2981
|
+
* BCP 47 locale used to format day/month names. When empty, the active
|
|
2982
|
+
* {@link MnLanguageService} locale is used.
|
|
2983
|
+
*/
|
|
2984
|
+
locale = input('', ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
2985
|
+
/** Emits when the user selects a new date. */
|
|
2986
|
+
dateSelected = output();
|
|
2987
|
+
/** The selected date formatted as YYYY-MM-DD for the date-picker input. */
|
|
2988
|
+
selectedDateString = computed(() => {
|
|
2989
|
+
const d = this.selectedDate();
|
|
2990
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
2991
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
2992
|
+
}, ...(ngDevMode ? [{ debugName: "selectedDateString" }] : []));
|
|
2993
|
+
destroyRef = inject(DestroyRef);
|
|
2994
|
+
lang = inject(MnLanguageService);
|
|
2995
|
+
/** Current viewport width in pixels, tracked so the tile count stays responsive. */
|
|
2996
|
+
viewportWidth = signal(this.getViewportWidth(), ...(ngDevMode ? [{ debugName: "viewportWidth" }] : []));
|
|
2997
|
+
/**
|
|
2998
|
+
* Whether the day strip (arrows + day tiles) is shown. Below
|
|
2999
|
+
* {@link MnDateSelectorBar.MIN_STRIP_WIDTH} it is hidden, leaving only the Today
|
|
3000
|
+
* button and the date picker.
|
|
3001
|
+
*/
|
|
3002
|
+
showDayStrip = computed(() => this.viewportWidth() >= MnDateSelectorBar.MIN_STRIP_WIDTH, ...(ngDevMode ? [{ debugName: "showDayStrip" }] : []));
|
|
3003
|
+
/** Start offset (in days from today) for the visible day-tile window. */
|
|
3004
|
+
dayOffset = signal(0, ...(ngDevMode ? [{ debugName: "dayOffset" }] : []));
|
|
3005
|
+
/** Bumped whenever the active language changes so name formatting re-runs. */
|
|
3006
|
+
localeTick = signal(0, ...(ngDevMode ? [{ debugName: "localeTick" }] : []));
|
|
3007
|
+
/**
|
|
3008
|
+
* Number of day tiles to render, resolved from {@link TILE_BREAKPOINTS} against
|
|
3009
|
+
* the current viewport width (3 → 5 → 7 as the screen widens).
|
|
3010
|
+
*/
|
|
3011
|
+
tileCount = computed(() => {
|
|
3012
|
+
const width = this.viewportWidth();
|
|
3013
|
+
return (TILE_BREAKPOINTS.find((bp) => width >= bp.minWidth)?.tiles ??
|
|
3014
|
+
TILE_BREAKPOINTS[TILE_BREAKPOINTS.length - 1].tiles);
|
|
3015
|
+
}, ...(ngDevMode ? [{ debugName: "tileCount" }] : []));
|
|
3016
|
+
/** Effective locale: explicit input, else the active app locale. */
|
|
3017
|
+
effectiveLocale = computed(() => {
|
|
3018
|
+
this.localeTick();
|
|
3019
|
+
return this.locale() || this.lang.locale;
|
|
3020
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveLocale" }] : []));
|
|
3021
|
+
/** The visible day tiles, derived from the selected date, offset and viewport. */
|
|
3022
|
+
dayTiles = computed(() => {
|
|
3023
|
+
const today = this.getToday();
|
|
3024
|
+
const selected = this.selectedDate();
|
|
3025
|
+
const offset = this.dayOffset();
|
|
3026
|
+
const count = this.tileCount();
|
|
3027
|
+
const locale = this.effectiveLocale();
|
|
3028
|
+
const tiles = [];
|
|
3029
|
+
for (let i = 0; i < count; i++) {
|
|
3030
|
+
const date = new Date(today);
|
|
3031
|
+
date.setDate(today.getDate() + offset + i);
|
|
3032
|
+
tiles.push({
|
|
3033
|
+
date,
|
|
3034
|
+
dayName: date.toLocaleDateString(locale, { weekday: 'short' }),
|
|
3035
|
+
dayNumber: date.getDate(),
|
|
3036
|
+
monthName: date.toLocaleDateString(locale, { month: 'short' }),
|
|
3037
|
+
isSelected: this.isSameDay(date, selected),
|
|
3038
|
+
isToday: this.isSameDay(date, today),
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
return tiles;
|
|
3042
|
+
}, ...(ngDevMode ? [{ debugName: "dayTiles" }] : []));
|
|
3043
|
+
ngOnInit() {
|
|
3044
|
+
// Re-format names when the active language changes.
|
|
3045
|
+
this.lang.locale$
|
|
3046
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
3047
|
+
.subscribe(() => this.localeTick.update((v) => v + 1));
|
|
3048
|
+
// Track viewport width so the tile count stays responsive.
|
|
3049
|
+
if (typeof window !== 'undefined') {
|
|
3050
|
+
fromEvent(window, 'resize')
|
|
3051
|
+
.pipe(debounceTime(100), takeUntilDestroyed(this.destroyRef))
|
|
3052
|
+
.subscribe(() => this.viewportWidth.set(this.getViewportWidth()));
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
/** Shifts the visible window back by one tile-count without changing the selection. */
|
|
3056
|
+
navigatePrevious() {
|
|
3057
|
+
this.dayOffset.update((o) => o - this.tileCount());
|
|
3058
|
+
}
|
|
3059
|
+
/** Shifts the visible window forward by one tile-count without changing the selection. */
|
|
3060
|
+
navigateNext() {
|
|
3061
|
+
this.dayOffset.update((o) => o + this.tileCount());
|
|
3062
|
+
}
|
|
3063
|
+
/** Resets the window to today and selects today. */
|
|
3064
|
+
goToToday() {
|
|
3065
|
+
this.dayOffset.set(0);
|
|
3066
|
+
// Only emit when today isn't already the selected day, so pressing "Today"
|
|
3067
|
+
// while on today doesn't trigger a redundant reload.
|
|
3068
|
+
if (!this.isSameDay(this.getToday(), this.selectedDate())) {
|
|
3069
|
+
this.dateSelected.emit(this.getToday());
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
/** Selects a date and emits it, unless it is already the selected day. */
|
|
3073
|
+
selectDate(date) {
|
|
3074
|
+
// Re-selecting the current day is a no-op: emitting again would make
|
|
3075
|
+
// consumers reload for no change.
|
|
3076
|
+
if (this.isSameDay(date, this.selectedDate()))
|
|
3077
|
+
return;
|
|
3078
|
+
this.dateSelected.emit(date);
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Handles the date-picker model change: centres the window on the picked date
|
|
3082
|
+
* and emits it (unless it is already the selected day).
|
|
3083
|
+
* @param value The date string in YYYY-MM-DD format.
|
|
3084
|
+
*/
|
|
3085
|
+
onDateModelChanged(value) {
|
|
3086
|
+
if (!value)
|
|
3087
|
+
return;
|
|
3088
|
+
const picked = new Date(value + 'T00:00:00');
|
|
3089
|
+
if (isNaN(picked.getTime()))
|
|
3090
|
+
return;
|
|
3091
|
+
const today = this.getToday();
|
|
3092
|
+
const diffDays = Math.round((picked.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
3093
|
+
// Centre the picked date in the visible tile window.
|
|
3094
|
+
this.dayOffset.set(diffDays - Math.floor(this.tileCount() / 2));
|
|
3095
|
+
// Picking the day that's already selected shouldn't re-emit.
|
|
3096
|
+
if (!this.isSameDay(picked, this.selectedDate())) {
|
|
3097
|
+
this.dateSelected.emit(picked);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Returns the current viewport width in pixels, falling back to a desktop-ish
|
|
3102
|
+
* width when there is no window (e.g. server-side rendering).
|
|
3103
|
+
*/
|
|
3104
|
+
getViewportWidth() {
|
|
3105
|
+
return typeof window === 'undefined' ? 1280 : window.innerWidth;
|
|
3106
|
+
}
|
|
3107
|
+
/** Returns a Date object for today at midnight. */
|
|
3108
|
+
getToday() {
|
|
3109
|
+
const now = new Date();
|
|
3110
|
+
now.setHours(0, 0, 0, 0);
|
|
3111
|
+
return now;
|
|
3112
|
+
}
|
|
3113
|
+
/** Checks whether two dates fall on the same calendar day. */
|
|
3114
|
+
isSameDay(a, b) {
|
|
3115
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
3116
|
+
a.getMonth() === b.getMonth() &&
|
|
3117
|
+
a.getDate() === b.getDate());
|
|
3118
|
+
}
|
|
3119
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDateSelectorBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3120
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDateSelectorBar, isStandalone: true, selector: "mn-date-selector-bar", inputs: { selectedDate: { classPropertyName: "selectedDate", publicName: "selectedDate", isSignal: true, isRequired: false, transformFunction: null }, todayLabel: { classPropertyName: "todayLabel", publicName: "todayLabel", isSignal: true, isRequired: false, transformFunction: null }, pickDateLabel: { classPropertyName: "pickDateLabel", publicName: "pickDateLabel", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dateSelected: "dateSelected" }, ngImport: i0, template: "<!-- Date selector bar -->\n<div\n class=\"flex flex-col min-[1500px]:flex-row min-[1500px]:items-center justify-center rounded-xl px-4 py-5 gap-x-6 gap-y-6 overflow-hidden\">\n <!-- Top row on mobile/tablet: Today + Date picker side by side -->\n <div class=\"flex min-[1500px]:contents items-center gap-3\">\n <!-- Today button -->\n <button (click)=\"goToToday()\" [data]=\"{ variant: 'fill', color: 'primary', size: 'lg' }\"\n class=\"flex-1 min-[1500px]:flex-none\"\n mnButton>\n {{ todayLabel() }}\n </button>\n\n <!-- Date picker (visible on mobile/tablet in top row, moves to end on desktop).\n Wrapped in a div that owns the responsive hide: with fullWidth the datetime sets an\n inline `display:block` on its host, which would override `min-[1500px]:hidden` and leave it\n visible on desktop (showing two pickers). Hiding the wrapper avoids that. -->\n <div class=\"min-[1500px]:hidden flex-1 min-w-0\">\n <mn-lib-datetime (ngModelChange)=\"onDateModelChanged($event)\"\n [ngModel]=\"selectedDateString()\"\n [props]=\"{ id: 'mn-dsb-date-mobile', mode: 'date', placeholder: pickDateLabel(), size: 'lg', borderRadius: 'xl', hover: true, fullWidth: true }\"\n class=\"block w-full\"></mn-lib-datetime>\n </div>\n </div>\n\n <!-- Date navigation (arrows + day tiles). Hidden on very narrow screens\n (< MIN_STRIP_WIDTH), where only the Today button and date picker remain. -->\n @if (showDayStrip()) {\n <div class=\"flex items-center gap-4 overflow-x-auto mx-auto min-[1500px]:mx-0 pb-2 max-w-full\">\n <!-- Left arrow: shifts the visible day window backward -->\n <button (click)=\"navigatePrevious()\" [data]=\"{ variant: 'outline', size: 'md', color: 'gray' }\" class=\"shrink-0\"\n mnButton>\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <!-- Day tiles -->\n @for (tile of dayTiles(); track tile.date.getTime()) {\n <button\n (click)=\"selectDate(tile.date)\"\n [class.bg-primary]=\"tile.isSelected\"\n [class.hover:bg-base-200]=\"!tile.isSelected\"\n [class.text-primary-content]=\"tile.isSelected\"\n class=\"flex flex-col items-center min-w-12 min-[1500px]:min-w-16 px-2 min-[1500px]:px-3 py-2 rounded-lg transition-all cursor-pointer shrink-0\"\n >\n <span class=\"text-xs font-medium uppercase\">{{ tile.dayName }}</span>\n <span class=\"text-base min-[1500px]:text-lg font-bold\">{{ tile.dayNumber }}</span>\n </button>\n }\n\n <!-- Right arrow: shifts the visible day window forward -->\n <button (click)=\"navigateNext()\" [data]=\"{ variant: 'outline', size: 'md', color: 'gray' }\" class=\"shrink-0\"\n mnButton>\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n }\n\n <!-- Vertical divider (hidden on mobile/tablet) -->\n <div class=\"hidden min-[1500px]:block w-px h-10 bg-base-300\"></div>\n\n <!-- Date picker (desktop only, hidden on mobile/tablet since it's in the top row) -->\n <mn-lib-datetime (ngModelChange)=\"onDateModelChanged($event)\"\n [ngModel]=\"selectedDateString()\"\n [props]=\"{ id: 'mn-dsb-date-desktop', mode: 'date', placeholder: pickDateLabel(), size: 'lg', borderRadius: 'xl', hover: true }\"\n class=\"hidden min-[1500px]:block\"></mn-lib-datetime>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: LucideChevronLeft, selector: "svg[lucideChevronLeft]" }, { kind: "component", type: LucideChevronRight, selector: "svg[lucideChevronRight]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3121
|
+
}
|
|
3122
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDateSelectorBar, decorators: [{
|
|
3123
|
+
type: Component,
|
|
3124
|
+
args: [{ selector: 'mn-date-selector-bar', standalone: true, imports: [
|
|
3125
|
+
CommonModule,
|
|
3126
|
+
FormsModule,
|
|
3127
|
+
MnButton,
|
|
3128
|
+
MnDatetime,
|
|
3129
|
+
LucideChevronLeft,
|
|
3130
|
+
LucideChevronRight,
|
|
3131
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Date selector bar -->\n<div\n class=\"flex flex-col min-[1500px]:flex-row min-[1500px]:items-center justify-center rounded-xl px-4 py-5 gap-x-6 gap-y-6 overflow-hidden\">\n <!-- Top row on mobile/tablet: Today + Date picker side by side -->\n <div class=\"flex min-[1500px]:contents items-center gap-3\">\n <!-- Today button -->\n <button (click)=\"goToToday()\" [data]=\"{ variant: 'fill', color: 'primary', size: 'lg' }\"\n class=\"flex-1 min-[1500px]:flex-none\"\n mnButton>\n {{ todayLabel() }}\n </button>\n\n <!-- Date picker (visible on mobile/tablet in top row, moves to end on desktop).\n Wrapped in a div that owns the responsive hide: with fullWidth the datetime sets an\n inline `display:block` on its host, which would override `min-[1500px]:hidden` and leave it\n visible on desktop (showing two pickers). Hiding the wrapper avoids that. -->\n <div class=\"min-[1500px]:hidden flex-1 min-w-0\">\n <mn-lib-datetime (ngModelChange)=\"onDateModelChanged($event)\"\n [ngModel]=\"selectedDateString()\"\n [props]=\"{ id: 'mn-dsb-date-mobile', mode: 'date', placeholder: pickDateLabel(), size: 'lg', borderRadius: 'xl', hover: true, fullWidth: true }\"\n class=\"block w-full\"></mn-lib-datetime>\n </div>\n </div>\n\n <!-- Date navigation (arrows + day tiles). Hidden on very narrow screens\n (< MIN_STRIP_WIDTH), where only the Today button and date picker remain. -->\n @if (showDayStrip()) {\n <div class=\"flex items-center gap-4 overflow-x-auto mx-auto min-[1500px]:mx-0 pb-2 max-w-full\">\n <!-- Left arrow: shifts the visible day window backward -->\n <button (click)=\"navigatePrevious()\" [data]=\"{ variant: 'outline', size: 'md', color: 'gray' }\" class=\"shrink-0\"\n mnButton>\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <!-- Day tiles -->\n @for (tile of dayTiles(); track tile.date.getTime()) {\n <button\n (click)=\"selectDate(tile.date)\"\n [class.bg-primary]=\"tile.isSelected\"\n [class.hover:bg-base-200]=\"!tile.isSelected\"\n [class.text-primary-content]=\"tile.isSelected\"\n class=\"flex flex-col items-center min-w-12 min-[1500px]:min-w-16 px-2 min-[1500px]:px-3 py-2 rounded-lg transition-all cursor-pointer shrink-0\"\n >\n <span class=\"text-xs font-medium uppercase\">{{ tile.dayName }}</span>\n <span class=\"text-base min-[1500px]:text-lg font-bold\">{{ tile.dayNumber }}</span>\n </button>\n }\n\n <!-- Right arrow: shifts the visible day window forward -->\n <button (click)=\"navigateNext()\" [data]=\"{ variant: 'outline', size: 'md', color: 'gray' }\" class=\"shrink-0\"\n mnButton>\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n }\n\n <!-- Vertical divider (hidden on mobile/tablet) -->\n <div class=\"hidden min-[1500px]:block w-px h-10 bg-base-300\"></div>\n\n <!-- Date picker (desktop only, hidden on mobile/tablet since it's in the top row) -->\n <mn-lib-datetime (ngModelChange)=\"onDateModelChanged($event)\"\n [ngModel]=\"selectedDateString()\"\n [props]=\"{ id: 'mn-dsb-date-desktop', mode: 'date', placeholder: pickDateLabel(), size: 'lg', borderRadius: 'xl', hover: true }\"\n class=\"hidden min-[1500px]:block\"></mn-lib-datetime>\n</div>\n" }]
|
|
3132
|
+
}], propDecorators: { selectedDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedDate", required: false }] }], todayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "todayLabel", required: false }] }], pickDateLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickDateLabel", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], dateSelected: [{ type: i0.Output, args: ["dateSelected"] }] } });
|
|
3133
|
+
|
|
2917
3134
|
const mnMultiSelectVariants = tv({
|
|
2918
3135
|
base: 'bg-base-100 border-1 border-base-300 text-base-content text-sm cursor-pointer',
|
|
2919
3136
|
variants: {
|
|
@@ -4701,7 +4918,7 @@ class MnCollectionBase {
|
|
|
4701
4918
|
this.cdr.markForCheck();
|
|
4702
4919
|
});
|
|
4703
4920
|
this.searchSubscription = this.searchSubject
|
|
4704
|
-
.pipe(debounceTime(300))
|
|
4921
|
+
.pipe(debounceTime$1(300))
|
|
4705
4922
|
.subscribe(value => {
|
|
4706
4923
|
this.searchValue = value;
|
|
4707
4924
|
this.applyFilter(true);
|
|
@@ -5318,11 +5535,11 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
5318
5535
|
});
|
|
5319
5536
|
}
|
|
5320
5537
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
5321
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" } }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [class.border-primary]=\"hasActiveFilters\"\n [class.text-primary]=\"hasActiveFilters\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5538
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" } }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5322
5539
|
}
|
|
5323
5540
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
5324
5541
|
type: Component,
|
|
5325
|
-
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [class.border-primary]=\"hasActiveFilters\"\n [class.text-primary]=\"hasActiveFilters\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n" }]
|
|
5542
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\n<div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[375px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\" class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm bg-base-200 px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px) -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"column.width ?? null\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n" }]
|
|
5326
5543
|
}], propDecorators: { sortChange: [{
|
|
5327
5544
|
type: Output
|
|
5328
5545
|
}], rowClick: [{
|
|
@@ -6137,6 +6354,14 @@ class MnWizardBodyComponent {
|
|
|
6137
6354
|
formBodies;
|
|
6138
6355
|
stepWrappers;
|
|
6139
6356
|
currentStepId;
|
|
6357
|
+
/**
|
|
6358
|
+
* Title of the currently active step, mirrored into a signal so the modal
|
|
6359
|
+
* shell can append it to the modal title on small screens (where the step
|
|
6360
|
+
* labels under the progress circles are hidden). A signal — not a getter —
|
|
6361
|
+
* because the shell lives outside this component's change-detection subtree,
|
|
6362
|
+
* so a plain field mutation here would not update the shell in a zoneless app.
|
|
6363
|
+
*/
|
|
6364
|
+
currentStepTitle = signal(undefined, ...(ngDevMode ? [{ debugName: "currentStepTitle" }] : []));
|
|
6140
6365
|
visitedStepIds = [];
|
|
6141
6366
|
isCurrentStepValid = true;
|
|
6142
6367
|
isCompleting = false;
|
|
@@ -6197,7 +6422,7 @@ class MnWizardBodyComponent {
|
|
|
6197
6422
|
};
|
|
6198
6423
|
}
|
|
6199
6424
|
}
|
|
6200
|
-
this.
|
|
6425
|
+
this.setCurrentStep(this.config.startStepId || this.config.steps[0]?.id);
|
|
6201
6426
|
if (this.currentStepId) {
|
|
6202
6427
|
this.visitedStepIds.push(this.currentStepId);
|
|
6203
6428
|
}
|
|
@@ -6225,6 +6450,25 @@ class MnWizardBodyComponent {
|
|
|
6225
6450
|
isTextBody(step) {
|
|
6226
6451
|
return typeof step.body === 'string';
|
|
6227
6452
|
}
|
|
6453
|
+
async goToStep(step) {
|
|
6454
|
+
if (!this.canNavigateToStep(step))
|
|
6455
|
+
return;
|
|
6456
|
+
if (step.id === this.currentStepId)
|
|
6457
|
+
return;
|
|
6458
|
+
// Validate current step's form before allowing direct step navigation
|
|
6459
|
+
const formBody = this.getCurrentFormBody();
|
|
6460
|
+
if (formBody && formBody.form.invalid) {
|
|
6461
|
+
formBody.form.markAllAsTouched();
|
|
6462
|
+
return;
|
|
6463
|
+
}
|
|
6464
|
+
const previousStepId = this.currentStepId;
|
|
6465
|
+
this.setCurrentStep(step.id);
|
|
6466
|
+
if (!this.visitedStepIds.includes(this.currentStepId)) {
|
|
6467
|
+
this.visitedStepIds.push(this.currentStepId);
|
|
6468
|
+
}
|
|
6469
|
+
await this.notifyStepChange(previousStepId, NavigationDirection.DIRECT);
|
|
6470
|
+
this.trackCurrentStepValidity();
|
|
6471
|
+
}
|
|
6228
6472
|
ngOnDestroy() {
|
|
6229
6473
|
this.statusSubscription?.unsubscribe();
|
|
6230
6474
|
this.formBodiesSubscription?.unsubscribe();
|
|
@@ -6296,23 +6540,43 @@ class MnWizardBodyComponent {
|
|
|
6296
6540
|
return false;
|
|
6297
6541
|
return this.isStepVisible(step);
|
|
6298
6542
|
}
|
|
6299
|
-
async
|
|
6300
|
-
if (!this.
|
|
6301
|
-
return;
|
|
6302
|
-
if (step.id === this.currentStepId)
|
|
6543
|
+
async next() {
|
|
6544
|
+
if (!this.canGoNext)
|
|
6303
6545
|
return;
|
|
6304
|
-
|
|
6546
|
+
const currentStep = this.currentStep;
|
|
6547
|
+
// Validate embedded form if present
|
|
6305
6548
|
const formBody = this.getCurrentFormBody();
|
|
6306
6549
|
if (formBody && formBody.form.invalid) {
|
|
6307
6550
|
formBody.form.markAllAsTouched();
|
|
6308
6551
|
return;
|
|
6309
6552
|
}
|
|
6553
|
+
if (currentStep?.guard?.canExit) {
|
|
6554
|
+
const canExit = await currentStep.guard.canExit();
|
|
6555
|
+
if (!canExit)
|
|
6556
|
+
return;
|
|
6557
|
+
}
|
|
6558
|
+
if (currentStep?.validators) {
|
|
6559
|
+
for (const validator of currentStep.validators) {
|
|
6560
|
+
const result = await validator.validate();
|
|
6561
|
+
if (result.status === 'invalid')
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
// Find next visible step
|
|
6566
|
+
const nextStep = this.visibleSteps[this.currentVisibleIndex + 1];
|
|
6567
|
+
if (!nextStep)
|
|
6568
|
+
return;
|
|
6569
|
+
if (nextStep.guard?.canEnter) {
|
|
6570
|
+
const canEnter = await nextStep.guard.canEnter();
|
|
6571
|
+
if (!canEnter)
|
|
6572
|
+
return;
|
|
6573
|
+
}
|
|
6310
6574
|
const previousStepId = this.currentStepId;
|
|
6311
|
-
this.
|
|
6575
|
+
this.setCurrentStep(nextStep.id);
|
|
6312
6576
|
if (!this.visitedStepIds.includes(this.currentStepId)) {
|
|
6313
6577
|
this.visitedStepIds.push(this.currentStepId);
|
|
6314
6578
|
}
|
|
6315
|
-
await this.notifyStepChange(previousStepId, NavigationDirection.
|
|
6579
|
+
await this.notifyStepChange(previousStepId, NavigationDirection.FORWARD);
|
|
6316
6580
|
this.trackCurrentStepValidity();
|
|
6317
6581
|
}
|
|
6318
6582
|
/** Find the MnFormBodyComponent for the current step */
|
|
@@ -6346,45 +6610,6 @@ class MnWizardBodyComponent {
|
|
|
6346
6610
|
}
|
|
6347
6611
|
this.cdr.detectChanges();
|
|
6348
6612
|
}
|
|
6349
|
-
async next() {
|
|
6350
|
-
if (!this.canGoNext)
|
|
6351
|
-
return;
|
|
6352
|
-
const currentStep = this.currentStep;
|
|
6353
|
-
// Validate embedded form if present
|
|
6354
|
-
const formBody = this.getCurrentFormBody();
|
|
6355
|
-
if (formBody && formBody.form.invalid) {
|
|
6356
|
-
formBody.form.markAllAsTouched();
|
|
6357
|
-
return;
|
|
6358
|
-
}
|
|
6359
|
-
if (currentStep?.guard?.canExit) {
|
|
6360
|
-
const canExit = await currentStep.guard.canExit();
|
|
6361
|
-
if (!canExit)
|
|
6362
|
-
return;
|
|
6363
|
-
}
|
|
6364
|
-
if (currentStep?.validators) {
|
|
6365
|
-
for (const validator of currentStep.validators) {
|
|
6366
|
-
const result = await validator.validate();
|
|
6367
|
-
if (result.status === 'invalid')
|
|
6368
|
-
return;
|
|
6369
|
-
}
|
|
6370
|
-
}
|
|
6371
|
-
// Find next visible step
|
|
6372
|
-
const nextStep = this.visibleSteps[this.currentVisibleIndex + 1];
|
|
6373
|
-
if (!nextStep)
|
|
6374
|
-
return;
|
|
6375
|
-
if (nextStep.guard?.canEnter) {
|
|
6376
|
-
const canEnter = await nextStep.guard.canEnter();
|
|
6377
|
-
if (!canEnter)
|
|
6378
|
-
return;
|
|
6379
|
-
}
|
|
6380
|
-
const previousStepId = this.currentStepId;
|
|
6381
|
-
this.currentStepId = nextStep.id;
|
|
6382
|
-
if (!this.visitedStepIds.includes(this.currentStepId)) {
|
|
6383
|
-
this.visitedStepIds.push(this.currentStepId);
|
|
6384
|
-
}
|
|
6385
|
-
await this.notifyStepChange(previousStepId, NavigationDirection.FORWARD);
|
|
6386
|
-
this.trackCurrentStepValidity();
|
|
6387
|
-
}
|
|
6388
6613
|
async back() {
|
|
6389
6614
|
if (!this.canGoBack) {
|
|
6390
6615
|
// Call onCancel handler if configured
|
|
@@ -6398,10 +6623,19 @@ class MnWizardBodyComponent {
|
|
|
6398
6623
|
const previousStepId = this.currentStepId;
|
|
6399
6624
|
// Remove current step from visited so the circle resets
|
|
6400
6625
|
this.visitedStepIds = this.visitedStepIds.filter(id => id !== this.currentStepId);
|
|
6401
|
-
this.
|
|
6626
|
+
this.setCurrentStep(prevStep.id);
|
|
6402
6627
|
await this.notifyStepChange(previousStepId, NavigationDirection.BACKWARD);
|
|
6403
6628
|
this.trackCurrentStepValidity();
|
|
6404
6629
|
}
|
|
6630
|
+
/**
|
|
6631
|
+
* Activates a step and mirrors its title into {@link currentStepTitle} so the
|
|
6632
|
+
* shell can reflect the step name in the modal title on small screens.
|
|
6633
|
+
* @param stepId Id of the step to make current.
|
|
6634
|
+
*/
|
|
6635
|
+
setCurrentStep(stepId) {
|
|
6636
|
+
this.currentStepId = stepId;
|
|
6637
|
+
this.currentStepTitle.set(this.config.steps.find(s => s.id === stepId)?.title);
|
|
6638
|
+
}
|
|
6405
6639
|
async complete() {
|
|
6406
6640
|
if (this.isCompleting)
|
|
6407
6641
|
return;
|
|
@@ -6649,6 +6883,14 @@ class MnModalShellComponent {
|
|
|
6649
6883
|
*/
|
|
6650
6884
|
isStacked = signal(false, ...(ngDevMode ? [{ debugName: "isStacked" }] : []));
|
|
6651
6885
|
ModalKind = ModalKind;
|
|
6886
|
+
/** The rendered wizard body, when this modal is a wizard — used to read the active step title. */
|
|
6887
|
+
wizardBody = viewChild(MnWizardBodyComponent, ...(ngDevMode ? [{ debugName: "wizardBody" }] : []));
|
|
6888
|
+
/**
|
|
6889
|
+
* Title of the wizard's current step, or undefined for non-wizard modals.
|
|
6890
|
+
* The template appends it to the modal title on small screens, where the
|
|
6891
|
+
* step labels under the progress circles are hidden.
|
|
6892
|
+
*/
|
|
6893
|
+
wizardStepTitle = computed(() => this.wizardBody()?.currentStepTitle(), ...(ngDevMode ? [{ debugName: "wizardStepTitle" }] : []));
|
|
6652
6894
|
previouslyFocusedElement = null;
|
|
6653
6895
|
focusTrapListener = null;
|
|
6654
6896
|
pollingTimer = null;
|
|
@@ -7044,7 +7286,7 @@ class MnModalShellComponent {
|
|
|
7044
7286
|
}
|
|
7045
7287
|
}
|
|
7046
7288
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7047
|
-
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" } }, 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<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\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 (click)=\"$event.stopPropagation()\"\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 <h2 class=\"m-0 text-xl font-semibold text-base-content\"
|
|
7289
|
+
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<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\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 (click)=\"$event.stopPropagation()\"\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 aria-label=\"Close modal\"\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 (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)}.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%;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}: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 .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"], outputs: ["actionClick"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
|
|
7048
7290
|
}
|
|
7049
7291
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
7050
7292
|
type: Component,
|
|
@@ -7057,12 +7299,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
7057
7299
|
MnFooterActionsComponent,
|
|
7058
7300
|
MnButton,
|
|
7059
7301
|
LucideX,
|
|
7060
|
-
], 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<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\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 (click)=\"$event.stopPropagation()\"\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 <h2 class=\"m-0 text-xl font-semibold text-base-content\"
|
|
7302
|
+
], 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<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\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 (click)=\"$event.stopPropagation()\"\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 aria-label=\"Close modal\"\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 (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)}.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%;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}: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 .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"] }]
|
|
7061
7303
|
}], propDecorators: { config: [{
|
|
7062
7304
|
type: Input
|
|
7063
7305
|
}], modalRef: [{
|
|
7064
7306
|
type: Input
|
|
7065
|
-
}], onEscapeKey: [{
|
|
7307
|
+
}], wizardBody: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MnWizardBodyComponent), { isSignal: true }] }], onEscapeKey: [{
|
|
7066
7308
|
type: HostListener,
|
|
7067
7309
|
args: ['document:keydown.escape', ['$event']]
|
|
7068
7310
|
}], hostClasses: [{
|
|
@@ -7269,6 +7511,7 @@ const DEFAULT_CALENDAR_CONFIG = (() => {
|
|
|
7269
7511
|
endHour: 22,
|
|
7270
7512
|
locale,
|
|
7271
7513
|
todayLabel: 'Today',
|
|
7514
|
+
pickDateLabel: 'Pick a date',
|
|
7272
7515
|
upcomingEventsTitle: 'Upcoming events',
|
|
7273
7516
|
noUpcomingEvents: 'No upcoming events',
|
|
7274
7517
|
viewLabels: { MONTH: 'Month', WEEK: 'Week', DAY: 'Day' },
|
|
@@ -7351,18 +7594,6 @@ function resolveCalendarConfig(partial) {
|
|
|
7351
7594
|
};
|
|
7352
7595
|
}
|
|
7353
7596
|
|
|
7354
|
-
/**
|
|
7355
|
-
* Injection token for the calendar date formatter.
|
|
7356
|
-
*
|
|
7357
|
-
* @example
|
|
7358
|
-
* ```ts
|
|
7359
|
-
* providers: [
|
|
7360
|
-
* { provide: CALENDAR_DATE_FORMATTER, useClass: MyCustomFormatter }
|
|
7361
|
-
* ]
|
|
7362
|
-
* ```
|
|
7363
|
-
*/
|
|
7364
|
-
const CALENDAR_DATE_FORMATTER = new InjectionToken('CalendarDateFormatter');
|
|
7365
|
-
|
|
7366
7597
|
/**
|
|
7367
7598
|
* Default implementation of {@link CalendarDateFormatter} that uses the
|
|
7368
7599
|
* browser's `Intl.DateTimeFormat` API for locale-aware formatting.
|
|
@@ -7718,6 +7949,18 @@ class CalendarUtility {
|
|
|
7718
7949
|
}
|
|
7719
7950
|
}
|
|
7720
7951
|
|
|
7952
|
+
/**
|
|
7953
|
+
* Injection token for the calendar date formatter.
|
|
7954
|
+
*
|
|
7955
|
+
* @example
|
|
7956
|
+
* ```ts
|
|
7957
|
+
* providers: [
|
|
7958
|
+
* { provide: CALENDAR_DATE_FORMATTER, useClass: MyCustomFormatter }
|
|
7959
|
+
* ]
|
|
7960
|
+
* ```
|
|
7961
|
+
*/
|
|
7962
|
+
const CALENDAR_DATE_FORMATTER = new InjectionToken('CalendarDateFormatter');
|
|
7963
|
+
|
|
7721
7964
|
/**
|
|
7722
7965
|
* Default event renderer used when no custom component is provided.
|
|
7723
7966
|
*
|
|
@@ -8334,7 +8577,6 @@ class CalendarViewComponent {
|
|
|
8334
8577
|
CalendarView = CalendarView;
|
|
8335
8578
|
currentView = CalendarView.WEEK;
|
|
8336
8579
|
focusDay = new Date();
|
|
8337
|
-
dateInputValue = '';
|
|
8338
8580
|
viewOptions = [];
|
|
8339
8581
|
isMobileView = false;
|
|
8340
8582
|
isTabletView = false;
|
|
@@ -8343,17 +8585,14 @@ class CalendarViewComponent {
|
|
|
8343
8585
|
/** Subject for broadcasting focus-day changes to child views. */
|
|
8344
8586
|
internalFocusDayChanged = new Subject();
|
|
8345
8587
|
destroy$ = new Subject();
|
|
8346
|
-
formatter;
|
|
8347
8588
|
config;
|
|
8348
8589
|
/** Reference to the injected mn-config object (mutated in-place on locale change). */
|
|
8349
8590
|
mnConfigRef;
|
|
8350
8591
|
destroyRef = inject(DestroyRef);
|
|
8351
8592
|
lang = inject(MnLanguageService);
|
|
8352
8593
|
constructor() {
|
|
8353
|
-
const formatter = inject(CALENDAR_DATE_FORMATTER, { optional: true });
|
|
8354
8594
|
const mnConfig = inject(MN_CALENDAR_CONFIG, { optional: true });
|
|
8355
8595
|
const legacyConfig = inject(CALENDAR_CONFIG, { optional: true });
|
|
8356
|
-
this.formatter = formatter ?? new DefaultCalendarDateFormatter();
|
|
8357
8596
|
// Keep a reference to the injected config so we can re-read it after locale changes.
|
|
8358
8597
|
this.mnConfigRef = mnConfig;
|
|
8359
8598
|
// Priority: mn-config system > legacy CALENDAR_CONFIG > built-in defaults
|
|
@@ -8371,7 +8610,6 @@ class CalendarViewComponent {
|
|
|
8371
8610
|
});
|
|
8372
8611
|
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
8373
8612
|
this.checkMobileView();
|
|
8374
|
-
this.updateDateInput();
|
|
8375
8613
|
this.RequestNewCalendarItemsEvent.emit(this.focusDay);
|
|
8376
8614
|
if (this.NewCalendarItemsEvent) {
|
|
8377
8615
|
this.NewCalendarItemsEvent.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
@@ -8391,54 +8629,9 @@ class CalendarViewComponent {
|
|
|
8391
8629
|
}
|
|
8392
8630
|
this.currentView = view;
|
|
8393
8631
|
}
|
|
8394
|
-
/**
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
switch (this.currentView) {
|
|
8398
|
-
case CalendarView.MONTH:
|
|
8399
|
-
d.setMonth(d.getMonth() - 1);
|
|
8400
|
-
break;
|
|
8401
|
-
case CalendarView.WEEK:
|
|
8402
|
-
d.setDate(d.getDate() - 7);
|
|
8403
|
-
break;
|
|
8404
|
-
case CalendarView.DAY:
|
|
8405
|
-
d.setDate(d.getDate() - 1);
|
|
8406
|
-
break;
|
|
8407
|
-
}
|
|
8408
|
-
this.setFocusDay(d);
|
|
8409
|
-
}
|
|
8410
|
-
/** Navigates to the next period (month / week / day). */
|
|
8411
|
-
navigateNext() {
|
|
8412
|
-
const d = new Date(this.focusDay);
|
|
8413
|
-
switch (this.currentView) {
|
|
8414
|
-
case CalendarView.MONTH:
|
|
8415
|
-
d.setMonth(d.getMonth() + 1);
|
|
8416
|
-
break;
|
|
8417
|
-
case CalendarView.WEEK:
|
|
8418
|
-
d.setDate(d.getDate() + 7);
|
|
8419
|
-
break;
|
|
8420
|
-
case CalendarView.DAY:
|
|
8421
|
-
d.setDate(d.getDate() + 1);
|
|
8422
|
-
break;
|
|
8423
|
-
}
|
|
8424
|
-
this.setFocusDay(d);
|
|
8425
|
-
}
|
|
8426
|
-
/** Navigates to today. */
|
|
8427
|
-
goToToday() {
|
|
8428
|
-
this.setFocusDay(new Date());
|
|
8429
|
-
}
|
|
8430
|
-
/** Handles the date-picker input change. */
|
|
8431
|
-
onDateInputChange(event) {
|
|
8432
|
-
const value = event.target.value;
|
|
8433
|
-
if (value) {
|
|
8434
|
-
this.setFocusDay(new Date(value));
|
|
8435
|
-
}
|
|
8436
|
-
}
|
|
8437
|
-
/** Handles the mn-lib-datetime ngModel change. */
|
|
8438
|
-
onDateStringChange(value) {
|
|
8439
|
-
if (value) {
|
|
8440
|
-
this.setFocusDay(new Date(value));
|
|
8441
|
-
}
|
|
8632
|
+
/** Handles a day selection from the date-selector bar. */
|
|
8633
|
+
onDateStripChange(date) {
|
|
8634
|
+
this.setFocusDay(date);
|
|
8442
8635
|
}
|
|
8443
8636
|
/** Handles a day click from the month view — switches to day view. */
|
|
8444
8637
|
onMonthDayClick(date) {
|
|
@@ -8476,17 +8669,13 @@ class CalendarViewComponent {
|
|
|
8476
8669
|
}
|
|
8477
8670
|
setFocusDay(date) {
|
|
8478
8671
|
this.focusDay = date;
|
|
8479
|
-
this.updateDateInput();
|
|
8480
8672
|
this.internalFocusDayChanged.next(date);
|
|
8481
8673
|
this.RequestNewCalendarItemsEvent.emit(date);
|
|
8482
8674
|
}
|
|
8483
|
-
updateDateInput() {
|
|
8484
|
-
this.dateInputValue = this.formatter.formatDateForFormControl(this.focusDay);
|
|
8485
|
-
}
|
|
8486
8675
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8487
8676
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarViewComponent, isStandalone: true, selector: "mn-calendar-view", inputs: { showButton: "showButton", buttonTitle: "buttonTitle", buttons: "buttons", CalendarEventComponent: "CalendarEventComponent", NewCalendarItemsEvent: "NewCalendarItemsEvent" }, outputs: { RequestNewCalendarItemsEvent: "RequestNewCalendarItemsEvent", CalendarItemClickedEvent: "CalendarItemClickedEvent", ButtonClickedEvent: "ButtonClickedEvent" }, host: { listeners: { "window:resize": "onResize()" } }, providers: [
|
|
8488
8677
|
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
8489
|
-
], ngImport: i0, template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n
|
|
8678
|
+
], ngImport: i0, template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar: date strip and calendar controls (view switcher + action buttons) share one row only on very\n wide screens (\u22651500px). The breakpoint matches the date-selector bar's own two-layer\u2192single-row switch\n so they flip in lockstep; a mismatched breakpoint left the controls inlined while the date bar was\n still stacked, producing a mixed layout. -->\n <div class=\"flex flex-col min-[1500px]:flex-row min-[1500px]:items-center gap-2 py-3\">\n <!-- Responsive date-selector bar: Today, day-window arrows, day tiles and a date picker. -->\n <mn-date-selector-bar\n (dateSelected)=\"onDateStripChange($event)\"\n [locale]=\"config.locale\"\n [pickDateLabel]=\"config.pickDateLabel\"\n [selectedDate]=\"focusDay\"\n [todayLabel]=\"config.todayLabel\"\n class=\"min-w-0 min-[1500px]:flex-1\">\n </mn-date-selector-bar>\n\n @if (!isMobileView || buttons.length || showButton) {\n <!-- order-first stacks the controls above the date strip when the toolbar wraps (below 1500px). -->\n <div class=\"flex items-center justify-end gap-2 shrink-0 order-first min-[1500px]:order-none\">\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div aria-label=\"Calendar view\" class=\"flex border border-base-300 rounded-md overflow-hidden\" role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n <div [class.flex-1]=\"isMobileView\" class=\"flex items-center gap-2\">\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [class.flex-1]=\"isMobileView\" [data]=\"btn.buttonData || {}\"\n mnButton>{{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [class.flex-1]=\"isMobileView\" [data]=\"{}\"\n mnButton>{{ buttonTitle }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CalendarMonthComponent, selector: "mn-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "mn-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "mn-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "mn-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnDateSelectorBar, selector: "mn-date-selector-bar", inputs: ["selectedDate", "todayLabel", "pickDateLabel", "locale"], outputs: ["dateSelected"] }] });
|
|
8490
8679
|
}
|
|
8491
8680
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
|
|
8492
8681
|
type: Component,
|
|
@@ -8497,11 +8686,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
8497
8686
|
CalendarDayComponent,
|
|
8498
8687
|
UpcomingEventsComponent,
|
|
8499
8688
|
MnButton,
|
|
8500
|
-
|
|
8501
|
-
FormsModule
|
|
8689
|
+
MnDateSelectorBar
|
|
8502
8690
|
], providers: [
|
|
8503
8691
|
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
8504
|
-
], template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n
|
|
8692
|
+
], template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar: date strip and calendar controls (view switcher + action buttons) share one row only on very\n wide screens (\u22651500px). The breakpoint matches the date-selector bar's own two-layer\u2192single-row switch\n so they flip in lockstep; a mismatched breakpoint left the controls inlined while the date bar was\n still stacked, producing a mixed layout. -->\n <div class=\"flex flex-col min-[1500px]:flex-row min-[1500px]:items-center gap-2 py-3\">\n <!-- Responsive date-selector bar: Today, day-window arrows, day tiles and a date picker. -->\n <mn-date-selector-bar\n (dateSelected)=\"onDateStripChange($event)\"\n [locale]=\"config.locale\"\n [pickDateLabel]=\"config.pickDateLabel\"\n [selectedDate]=\"focusDay\"\n [todayLabel]=\"config.todayLabel\"\n class=\"min-w-0 min-[1500px]:flex-1\">\n </mn-date-selector-bar>\n\n @if (!isMobileView || buttons.length || showButton) {\n <!-- order-first stacks the controls above the date strip when the toolbar wraps (below 1500px). -->\n <div class=\"flex items-center justify-end gap-2 shrink-0 order-first min-[1500px]:order-none\">\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div aria-label=\"Calendar view\" class=\"flex border border-base-300 rounded-md overflow-hidden\" role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n <div [class.flex-1]=\"isMobileView\" class=\"flex items-center gap-2\">\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [class.flex-1]=\"isMobileView\" [data]=\"btn.buttonData || {}\"\n mnButton>{{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [class.flex-1]=\"isMobileView\" [data]=\"{}\"\n mnButton>{{ buttonTitle }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"] }]
|
|
8505
8693
|
}], ctorParameters: () => [], propDecorators: { showButton: [{
|
|
8506
8694
|
type: Input
|
|
8507
8695
|
}], buttonTitle: [{
|
|
@@ -9256,5 +9444,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
|
9256
9444
|
* Generated bundle index. Do not edit.
|
|
9257
9445
|
*/
|
|
9258
9446
|
|
|
9259
|
-
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_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, 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, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
9447
|
+
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_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, 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, 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, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
9260
9448
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|