mn-angular-lib 1.0.5 → 1.0.6
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 +267 -22
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mn-angular-lib.d.ts +184 -5
|
@@ -2515,6 +2515,204 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
2515
2515
|
args: ['window:resize', []]
|
|
2516
2516
|
}] } });
|
|
2517
2517
|
|
|
2518
|
+
const mnSelectVariants = tv({
|
|
2519
|
+
base: 'bg-base-100 border-1 border-base-300 text-base-content text-sm cursor-pointer',
|
|
2520
|
+
variants: {
|
|
2521
|
+
shadow: {
|
|
2522
|
+
true: 'shadow-lg',
|
|
2523
|
+
},
|
|
2524
|
+
size: {
|
|
2525
|
+
sm: 'p-2',
|
|
2526
|
+
md: 'p-3',
|
|
2527
|
+
lg: 'p-4',
|
|
2528
|
+
},
|
|
2529
|
+
borderRadius: {
|
|
2530
|
+
none: 'rounded-none',
|
|
2531
|
+
xs: 'rounded-xs',
|
|
2532
|
+
sm: 'rounded-sm',
|
|
2533
|
+
md: 'rounded-md',
|
|
2534
|
+
lg: 'rounded-lg',
|
|
2535
|
+
xl: 'rounded-xl',
|
|
2536
|
+
two_xl: 'rounded-2xl',
|
|
2537
|
+
three_xl: 'rounded-3xl',
|
|
2538
|
+
four_xl: 'rounded-4xl',
|
|
2539
|
+
},
|
|
2540
|
+
fullWidth: {
|
|
2541
|
+
true: 'w-full',
|
|
2542
|
+
},
|
|
2543
|
+
},
|
|
2544
|
+
defaultVariants: {
|
|
2545
|
+
size: 'md',
|
|
2546
|
+
borderRadius: 'md',
|
|
2547
|
+
},
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2550
|
+
const MN_SELECT_CONFIG = new InjectionToken('MN_SELECT_CONFIG');
|
|
2551
|
+
class MnSelect {
|
|
2552
|
+
ngControl;
|
|
2553
|
+
props;
|
|
2554
|
+
/** Currently selected value */
|
|
2555
|
+
selectedValue = null;
|
|
2556
|
+
isDisabled = false;
|
|
2557
|
+
uiConfig = {};
|
|
2558
|
+
configService = inject(MnConfigService);
|
|
2559
|
+
sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
|
|
2560
|
+
explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
|
|
2561
|
+
lang = inject(MnLanguageService);
|
|
2562
|
+
destroyRef = inject(DestroyRef);
|
|
2563
|
+
builtInErrorMessages = {
|
|
2564
|
+
required: 'Please select an option',
|
|
2565
|
+
};
|
|
2566
|
+
constructor(ngControl) {
|
|
2567
|
+
this.ngControl = ngControl;
|
|
2568
|
+
if (this.ngControl)
|
|
2569
|
+
this.ngControl.valueAccessor = this;
|
|
2570
|
+
}
|
|
2571
|
+
get selectedOption() {
|
|
2572
|
+
return this.props.options.find(o => o.value === this.selectedValue);
|
|
2573
|
+
}
|
|
2574
|
+
get control() {
|
|
2575
|
+
return this.ngControl?.control ?? null;
|
|
2576
|
+
}
|
|
2577
|
+
get showError() {
|
|
2578
|
+
const c = this.control;
|
|
2579
|
+
return !!c && c.invalid && (c.touched || c.dirty);
|
|
2580
|
+
}
|
|
2581
|
+
get errorMessages() {
|
|
2582
|
+
const errors = this.control?.errors;
|
|
2583
|
+
if (!errors)
|
|
2584
|
+
return [];
|
|
2585
|
+
return Object.keys(errors).map(key => this.resolveErrorMessageForKey(key, errors));
|
|
2586
|
+
}
|
|
2587
|
+
// ========== ControlValueAccessor Implementation ==========
|
|
2588
|
+
get errorMessage() {
|
|
2589
|
+
const errors = this.control?.errors;
|
|
2590
|
+
if (!errors)
|
|
2591
|
+
return null;
|
|
2592
|
+
const errorKey = this.pickErrorKey(errors);
|
|
2593
|
+
return this.resolveErrorMessageForKey(errorKey, errors);
|
|
2594
|
+
}
|
|
2595
|
+
get resolvedId() {
|
|
2596
|
+
return this.props.id;
|
|
2597
|
+
}
|
|
2598
|
+
get resolvedName() {
|
|
2599
|
+
return this.props?.name ?? null;
|
|
2600
|
+
}
|
|
2601
|
+
get selectClasses() {
|
|
2602
|
+
return mnSelectVariants({
|
|
2603
|
+
size: this.props.size,
|
|
2604
|
+
borderRadius: this.props.borderRadius,
|
|
2605
|
+
shadow: this.props.shadow,
|
|
2606
|
+
fullWidth: this.props.fullWidth,
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
// ========== Select Logic ==========
|
|
2610
|
+
ngOnInit() {
|
|
2611
|
+
this.resolveConfig();
|
|
2612
|
+
const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
|
|
2613
|
+
this.resolveConfig();
|
|
2614
|
+
});
|
|
2615
|
+
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
2616
|
+
}
|
|
2617
|
+
writeValue(val) {
|
|
2618
|
+
this.selectedValue = val ?? null;
|
|
2619
|
+
}
|
|
2620
|
+
registerOnChange(fn) {
|
|
2621
|
+
this.onChange = fn;
|
|
2622
|
+
}
|
|
2623
|
+
registerOnTouched(fn) {
|
|
2624
|
+
this.onTouched = fn;
|
|
2625
|
+
}
|
|
2626
|
+
setDisabledState(isDisabled) {
|
|
2627
|
+
this.isDisabled = isDisabled;
|
|
2628
|
+
}
|
|
2629
|
+
// ========== Error Handling ==========
|
|
2630
|
+
/** Returns the index of an option, used as the <option> value attribute */
|
|
2631
|
+
optionIndex(option) {
|
|
2632
|
+
return String(this.props.options.indexOf(option));
|
|
2633
|
+
}
|
|
2634
|
+
/** Handles native select change event */
|
|
2635
|
+
onSelectChange(event) {
|
|
2636
|
+
const target = event.target;
|
|
2637
|
+
const index = parseInt(target.value, 10);
|
|
2638
|
+
if (isNaN(index) || index < 0 || index >= this.props.options.length) {
|
|
2639
|
+
this.selectedValue = null;
|
|
2640
|
+
this.onChange(null);
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const option = this.props.options[index];
|
|
2644
|
+
if (option.disabled)
|
|
2645
|
+
return;
|
|
2646
|
+
this.selectedValue = option.value;
|
|
2647
|
+
this.onChange(this.selectedValue);
|
|
2648
|
+
}
|
|
2649
|
+
isSelected(option) {
|
|
2650
|
+
return this.selectedValue === option.value;
|
|
2651
|
+
}
|
|
2652
|
+
handleBlur() {
|
|
2653
|
+
this.onTouched();
|
|
2654
|
+
}
|
|
2655
|
+
isRequired() {
|
|
2656
|
+
if (!this.control)
|
|
2657
|
+
return false;
|
|
2658
|
+
return this.control.hasValidator(Validators.required);
|
|
2659
|
+
}
|
|
2660
|
+
onChange = () => {
|
|
2661
|
+
};
|
|
2662
|
+
onTouched = () => {
|
|
2663
|
+
};
|
|
2664
|
+
// ========== Resolved Properties ==========
|
|
2665
|
+
resolveConfig() {
|
|
2666
|
+
const instanceId = this.explicitInstanceId || `mn-select-${this.props.id}`;
|
|
2667
|
+
this.uiConfig = this.configService.resolve('mn-select', this.sectionPath, instanceId);
|
|
2668
|
+
if (this.props.label) {
|
|
2669
|
+
this.uiConfig = { ...this.uiConfig, label: this.props.label };
|
|
2670
|
+
}
|
|
2671
|
+
if (this.props.placeholder) {
|
|
2672
|
+
this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
pickErrorKey(errors) {
|
|
2676
|
+
if (this.props.errorPriority) {
|
|
2677
|
+
for (const key of this.props.errorPriority) {
|
|
2678
|
+
if (errors[key] !== undefined) {
|
|
2679
|
+
return key;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return Object.keys(errors)[0];
|
|
2684
|
+
}
|
|
2685
|
+
resolveErrorMessageForKey(errorKey, errors) {
|
|
2686
|
+
const errorArgs = errors[errorKey];
|
|
2687
|
+
const customMsg = this.props.errorMessages?.[errorKey];
|
|
2688
|
+
const configMsg = this.uiConfig.errorMessages?.[errorKey];
|
|
2689
|
+
const useBuiltIn = this.props.useBuiltInErrorMessages !== false;
|
|
2690
|
+
const builtInMsg = useBuiltIn ? this.builtInErrorMessages[errorKey] : undefined;
|
|
2691
|
+
const fallbackMsg = this.props.defaultErrorMessage;
|
|
2692
|
+
const msgDef = customMsg ?? configMsg ?? builtInMsg ?? fallbackMsg ?? 'Invalid input';
|
|
2693
|
+
if (typeof msgDef === 'function') {
|
|
2694
|
+
return msgDef(errorArgs, errors);
|
|
2695
|
+
}
|
|
2696
|
+
if (errorArgs && typeof errorArgs === 'object') {
|
|
2697
|
+
return msgDef.replace(/{{(\w+)}}/g, (_, key) => errorArgs[key] ?? _);
|
|
2698
|
+
}
|
|
2699
|
+
return msgDef;
|
|
2700
|
+
}
|
|
2701
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
2702
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSelect, isStandalone: true, selector: "mn-lib-select", inputs: { props: "props" }, ngImport: i0, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <select\n (blur)=\"handleBlur()\"\n (change)=\"onSelectChange($event)\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n [name]=\"resolvedName\"\n [ngClass]=\"selectClasses\"\n >\n @if (props.placeholder || uiConfig.placeholder) {\n <option [selected]=\"selectedValue == null\" [value]=\"''\" disabled>\n {{ uiConfig.placeholder || props.placeholder }}\n </option>\n }\n @for (opt of props.options; track opt.value) {\n <option\n [disabled]=\"opt.disabled\"\n [selected]=\"isSelected(opt)\"\n [value]=\"optionIndex(opt)\"\n >{{ opt.label }}\n </option>\n }\n </select>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
2703
|
+
}
|
|
2704
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, decorators: [{
|
|
2705
|
+
type: Component,
|
|
2706
|
+
args: [{ selector: 'mn-lib-select', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <select\n (blur)=\"handleBlur()\"\n (change)=\"onSelectChange($event)\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [disabled]=\"isDisabled\"\n [id]=\"resolvedId\"\n [name]=\"resolvedName\"\n [ngClass]=\"selectClasses\"\n >\n @if (props.placeholder || uiConfig.placeholder) {\n <option [selected]=\"selectedValue == null\" [value]=\"''\" disabled>\n {{ uiConfig.placeholder || props.placeholder }}\n </option>\n }\n @for (opt of props.options; track opt.value) {\n <option\n [disabled]=\"opt.disabled\"\n [selected]=\"isSelected(opt)\"\n [value]=\"optionIndex(opt)\"\n >{{ opt.label }}\n </option>\n }\n </select>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
2707
|
+
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
2708
|
+
type: Optional
|
|
2709
|
+
}, {
|
|
2710
|
+
type: Self
|
|
2711
|
+
}] }], propDecorators: { props: [{
|
|
2712
|
+
type: Input,
|
|
2713
|
+
args: [{ required: true }]
|
|
2714
|
+
}] } });
|
|
2715
|
+
|
|
2518
2716
|
// =========================
|
|
2519
2717
|
// Enums / Value Types
|
|
2520
2718
|
// =========================
|
|
@@ -3764,6 +3962,18 @@ class MnTable {
|
|
|
3764
3962
|
get resolvedPageSizeOptions() {
|
|
3765
3963
|
return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
3766
3964
|
}
|
|
3965
|
+
/** Page-size options formatted for mn-select. */
|
|
3966
|
+
get pageSizeSelectOptions() {
|
|
3967
|
+
return this.resolvedPageSizeOptions.map(opt => ({ label: String(opt), value: opt }));
|
|
3968
|
+
}
|
|
3969
|
+
/** Filter options formatted for mn-select for a given column. */
|
|
3970
|
+
getFilterSelectOptions(column) {
|
|
3971
|
+
const placeholder = column.filterPlaceholder ?? 'All';
|
|
3972
|
+
return [
|
|
3973
|
+
{ label: placeholder, value: '' },
|
|
3974
|
+
...(column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) })),
|
|
3975
|
+
];
|
|
3976
|
+
}
|
|
3767
3977
|
goToPage(page) {
|
|
3768
3978
|
if (page < 1 || page > this.totalPages)
|
|
3769
3979
|
return;
|
|
@@ -3927,6 +4137,14 @@ class MnTable {
|
|
|
3927
4137
|
throw new Error(`[MnTable] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
|
|
3928
4138
|
}
|
|
3929
4139
|
}
|
|
4140
|
+
// Validate pageSize is one of pageSizeOptions when pagination is active
|
|
4141
|
+
if (mode && mode !== 'none') {
|
|
4142
|
+
const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
4143
|
+
const size = this.dataSource.pageSize ?? 10;
|
|
4144
|
+
if (!options.includes(size)) {
|
|
4145
|
+
throw new Error(`[MnTable] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
3930
4148
|
}
|
|
3931
4149
|
emitSelection() {
|
|
3932
4150
|
const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
@@ -3934,11 +4152,11 @@ class MnTable {
|
|
|
3934
4152
|
this.selectionChange.emit(rows);
|
|
3935
4153
|
}
|
|
3936
4154
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3937
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", inputs: { dataSource: "dataSource" }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick" }, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 -->\n @if (hasColumnFilters) {\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-sm text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer \"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4155
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", inputs: { dataSource: "dataSource" }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick" }, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 -->\n @if (hasColumnFilters) {\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 (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-table-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3938
4156
|
}
|
|
3939
4157
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
3940
4158
|
type: Component,
|
|
3941
|
-
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnHiddenBelowDirective, MnInputField, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 -->\n @if (hasColumnFilters) {\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-sm text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer \"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n" }]
|
|
4159
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnHiddenBelowDirective, MnInputField, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 -->\n @if (hasColumnFilters) {\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 (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-table-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n" }]
|
|
3942
4160
|
}], propDecorators: { dataSource: [{
|
|
3943
4161
|
type: Input
|
|
3944
4162
|
}], sortChange: [{
|
|
@@ -4263,7 +4481,7 @@ class MnFormBodyComponent {
|
|
|
4263
4481
|
}
|
|
4264
4482
|
isGroupVisible(group) {
|
|
4265
4483
|
const key = group.title || `group-${this.fieldGroups.indexOf(group)}`;
|
|
4266
|
-
return this.groupVisibility[key]
|
|
4484
|
+
return this.groupVisibility[key];
|
|
4267
4485
|
}
|
|
4268
4486
|
// =========================
|
|
4269
4487
|
// Feature 1: Conditional/Dynamic Fields
|
|
@@ -4309,7 +4527,7 @@ class MnFormBodyComponent {
|
|
|
4309
4527
|
});
|
|
4310
4528
|
}
|
|
4311
4529
|
isFieldVisible(field) {
|
|
4312
|
-
return this.fieldVisibility[field.key]
|
|
4530
|
+
return this.fieldVisibility[field.key];
|
|
4313
4531
|
}
|
|
4314
4532
|
// =========================
|
|
4315
4533
|
// Feature 2: Cross-Field Validation
|
|
@@ -4343,11 +4561,37 @@ class MnFormBodyComponent {
|
|
|
4343
4561
|
}
|
|
4344
4562
|
});
|
|
4345
4563
|
}
|
|
4564
|
+
isFieldLoading(key) {
|
|
4565
|
+
return this.fieldLoading[key];
|
|
4566
|
+
}
|
|
4567
|
+
/** Get options for a field — uses dataSource options if available, otherwise static options */
|
|
4568
|
+
getFieldOptions(field) {
|
|
4569
|
+
const key = field.key;
|
|
4570
|
+
if (this.fieldOptions[key] !== undefined) {
|
|
4571
|
+
return this.fieldOptions[key];
|
|
4572
|
+
}
|
|
4573
|
+
return field.options || [];
|
|
4574
|
+
}
|
|
4575
|
+
/** Convert SelectOption[] to MnSelectOption[] for mn-lib-select */
|
|
4576
|
+
getSelectOptions(field) {
|
|
4577
|
+
return this.getFieldOptions(field).map(o => ({
|
|
4578
|
+
label: o.label,
|
|
4579
|
+
value: o.value,
|
|
4580
|
+
disabled: o.state === 'disabled',
|
|
4581
|
+
}));
|
|
4582
|
+
}
|
|
4583
|
+
getSelectProps(field) {
|
|
4584
|
+
return {
|
|
4585
|
+
id: field.key,
|
|
4586
|
+
label: this.asField(field).label,
|
|
4587
|
+
placeholder: this.isFieldLoading(field.key) ? this.labels.loading : this.labels.selectPlaceholder,
|
|
4588
|
+
options: this.getSelectOptions(field),
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4346
4591
|
async loadFieldOptions(key, dataSource, formValue) {
|
|
4347
4592
|
this.fieldLoading[key] = true;
|
|
4348
4593
|
try {
|
|
4349
|
-
|
|
4350
|
-
this.fieldOptions[key] = options;
|
|
4594
|
+
this.fieldOptions[key] = await dataSource.load(formValue);
|
|
4351
4595
|
}
|
|
4352
4596
|
catch (error) {
|
|
4353
4597
|
console.error(`Failed to load options for field '${key}':`, error);
|
|
@@ -4357,17 +4601,6 @@ class MnFormBodyComponent {
|
|
|
4357
4601
|
this.fieldLoading[key] = false;
|
|
4358
4602
|
}
|
|
4359
4603
|
}
|
|
4360
|
-
/** Get options for a field — uses dataSource options if available, otherwise static options */
|
|
4361
|
-
getFieldOptions(field) {
|
|
4362
|
-
const key = field.key;
|
|
4363
|
-
if (this.fieldOptions[key] !== undefined) {
|
|
4364
|
-
return this.fieldOptions[key];
|
|
4365
|
-
}
|
|
4366
|
-
return field.options || [];
|
|
4367
|
-
}
|
|
4368
|
-
isFieldLoading(key) {
|
|
4369
|
-
return this.fieldLoading[key] === true;
|
|
4370
|
-
}
|
|
4371
4604
|
// =========================
|
|
4372
4605
|
// Feature: Multi-Select Table Fields
|
|
4373
4606
|
// =========================
|
|
@@ -4614,11 +4847,11 @@ class MnFormBodyComponent {
|
|
|
4614
4847
|
}
|
|
4615
4848
|
}
|
|
4616
4849
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, deps: [{ token: i1$2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component });
|
|
4617
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-base-300 rounded-xl text-base text-base-content bg-base-100 transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key)) || isFieldDisabled(rowField.field)\"\n [attr.disabled]=\"isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field) ? '' : null\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n @for (option of getFieldOptions(rowField.field); track option.value) {\n <option\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n }\n </select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-1\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 border-t border-base-300\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", inputs: ["dataSource"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
|
|
4850
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-1\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 border-t border-base-300\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", inputs: ["dataSource"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
|
|
4618
4851
|
}
|
|
4619
4852
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
|
|
4620
4853
|
type: Component,
|
|
4621
|
-
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnCustomFieldHostDirective, MnTable, MnCustomBodyHostComponent], template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-base-300 rounded-xl text-base text-base-content bg-base-100 transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key)) || isFieldDisabled(rowField.field)\"\n [attr.disabled]=\"isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field) ? '' : null\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n @for (option of getFieldOptions(rowField.field); track option.value) {\n <option\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n }\n </select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-1\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 border-t border-base-300\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"] }]
|
|
4854
|
+
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnSelect, MnCustomFieldHostDirective, MnTable, MnCustomBodyHostComponent], template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-1\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 border-t border-base-300\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"] }]
|
|
4622
4855
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
|
|
4623
4856
|
type: Input
|
|
4624
4857
|
}], modalRef: [{
|
|
@@ -5678,6 +5911,10 @@ class MnList {
|
|
|
5678
5911
|
get resolvedPageSizeOptions() {
|
|
5679
5912
|
return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
5680
5913
|
}
|
|
5914
|
+
/** Page-size options formatted for mn-select. */
|
|
5915
|
+
get pageSizeSelectOptions() {
|
|
5916
|
+
return this.resolvedPageSizeOptions.map(opt => ({ label: String(opt), value: opt }));
|
|
5917
|
+
}
|
|
5681
5918
|
goToPage(page) {
|
|
5682
5919
|
if (page < 1 || page > this.totalPages)
|
|
5683
5920
|
return;
|
|
@@ -5769,6 +6006,14 @@ class MnList {
|
|
|
5769
6006
|
throw new Error(`[MnList] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
|
|
5770
6007
|
}
|
|
5771
6008
|
}
|
|
6009
|
+
// Validate pageSize is one of pageSizeOptions when pagination is active
|
|
6010
|
+
if (mode && mode !== 'none') {
|
|
6011
|
+
const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
6012
|
+
const size = this.dataSource.pageSize ?? 10;
|
|
6013
|
+
if (!options.includes(size)) {
|
|
6014
|
+
throw new Error(`[MnList] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
5772
6017
|
}
|
|
5773
6018
|
emitSelection() {
|
|
5774
6019
|
const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
@@ -5776,11 +6021,11 @@ class MnList {
|
|
|
5776
6021
|
this.selectionChange.emit(rows);
|
|
5777
6022
|
}
|
|
5778
6023
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5779
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", inputs: { dataSource: "dataSource" }, outputs: { selectionChange: "selectionChange", itemClick: "itemClick" }, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div class=\"animate-pulse px-4 py-3\" [class.py-2]=\"dataSource.appearance?.compact\" role=\"listitem\">\n <div class=\"h-4 w-3/4 rounded bg-base-300 mb-1\"></div>\n <div class=\"h-3 w-1/2 rounded bg-base-300\"></div>\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Items per page:' }}</span>\n <
|
|
6024
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", inputs: { dataSource: "dataSource" }, outputs: { selectionChange: "selectionChange", itemClick: "itemClick" }, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div class=\"animate-pulse px-4 py-3\" [class.py-2]=\"dataSource.appearance?.compact\" role=\"listitem\">\n <div class=\"h-4 w-3/4 rounded bg-base-300 mb-1\"></div>\n <div class=\"h-3 w-1/2 rounded bg-base-300\"></div>\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-list-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5780
6025
|
}
|
|
5781
6026
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
|
|
5782
6027
|
type: Component,
|
|
5783
|
-
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div class=\"animate-pulse px-4 py-3\" [class.py-2]=\"dataSource.appearance?.compact\" role=\"listitem\">\n <div class=\"h-4 w-3/4 rounded bg-base-300 mb-1\"></div>\n <div class=\"h-3 w-1/2 rounded bg-base-300\"></div>\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Items per page:' }}</span>\n <
|
|
6028
|
+
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div class=\"animate-pulse px-4 py-3\" [class.py-2]=\"dataSource.appearance?.compact\" role=\"listitem\">\n <div class=\"h-4 w-3/4 rounded bg-base-300 mb-1\"></div>\n <div class=\"h-3 w-1/2 rounded bg-base-300\"></div>\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\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 </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-list-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n" }]
|
|
5784
6029
|
}], propDecorators: { dataSource: [{
|
|
5785
6030
|
type: Input
|
|
5786
6031
|
}], selectionChange: [{
|
|
@@ -7525,5 +7770,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
|
7525
7770
|
* Generated bundle index. Do not edit.
|
|
7526
7771
|
*/
|
|
7527
7772
|
|
|
7528
|
-
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_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnHiddenBelowDirective, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, 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, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
7773
|
+
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_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, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnHiddenBelowDirective, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, 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, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
7529
7774
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|