mn-angular-lib 0.0.50 → 0.0.52
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.
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, DestroyRef, Self, APP_INITIALIZER,
|
|
2
|
+
import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, signal, ElementRef, DestroyRef, Self, APP_INITIALIZER, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewContainerRef, ViewChild, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
|
|
3
3
|
export { TemplateRef, Type } from '@angular/core';
|
|
4
4
|
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, map, catchError, of } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgClass, NgOptimizedImage, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
|
7
7
|
import { tv } from 'tailwind-variants';
|
|
8
8
|
import * as i1$2 from '@angular/forms';
|
|
9
|
-
import { Validators, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
|
9
|
+
import { Validators, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
|
10
10
|
import * as i1$1 from '@angular/common/http';
|
|
11
11
|
import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
|
|
12
12
|
import JSON5 from 'json5';
|
|
@@ -401,12 +401,65 @@ const emptyToNull = (raw) => (raw === '' ? null : raw);
|
|
|
401
401
|
* - Values are stored as strings in the FormControl
|
|
402
402
|
* - No special DOM attributes
|
|
403
403
|
* - No additional validation (relies on Angular's built-in validators)
|
|
404
|
+
* - Supports simple masking (0 for digit, A for alpha, * for any)
|
|
404
405
|
*/
|
|
405
406
|
const defaultTextAdapter = {
|
|
406
407
|
parse: (raw) => emptyToNull(raw),
|
|
407
408
|
format: (val) => (val == null ? '' : String(val)),
|
|
408
409
|
attrs: () => ({}),
|
|
409
410
|
validate: () => null,
|
|
411
|
+
applyMask: (value, mask) => {
|
|
412
|
+
if (!mask || !value)
|
|
413
|
+
return value;
|
|
414
|
+
let result = '';
|
|
415
|
+
let maskIndex = 0;
|
|
416
|
+
let dataIndex = 0;
|
|
417
|
+
// Remove non-alphanumeric if we want to re-mask from clean data
|
|
418
|
+
// But usually we just want to restrict input.
|
|
419
|
+
// A simple implementation:
|
|
420
|
+
while (maskIndex < mask.length && dataIndex < value.length) {
|
|
421
|
+
const maskChar = mask[maskIndex];
|
|
422
|
+
const dataChar = value[dataIndex];
|
|
423
|
+
if (maskChar === '0') {
|
|
424
|
+
if (/\d/.test(dataChar)) {
|
|
425
|
+
result += dataChar;
|
|
426
|
+
dataIndex++;
|
|
427
|
+
maskIndex++;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
dataIndex++; // skip invalid
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (maskChar === 'A') {
|
|
434
|
+
if (/[a-zA-Z]/.test(dataChar)) {
|
|
435
|
+
result += dataChar;
|
|
436
|
+
dataIndex++;
|
|
437
|
+
maskIndex++;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
dataIndex++; // skip invalid
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else if (maskChar === '*') {
|
|
444
|
+
result += dataChar;
|
|
445
|
+
dataIndex++;
|
|
446
|
+
maskIndex++;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
result += maskChar;
|
|
450
|
+
if (dataChar === maskChar) {
|
|
451
|
+
dataIndex++;
|
|
452
|
+
}
|
|
453
|
+
maskIndex++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Auto-append static characters if next in mask
|
|
457
|
+
while (maskIndex < mask.length && !/[0A*]/.test(mask[maskIndex])) {
|
|
458
|
+
result += mask[maskIndex];
|
|
459
|
+
maskIndex++;
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
410
463
|
};
|
|
411
464
|
/**
|
|
412
465
|
* Adapter for date and time input types.
|
|
@@ -667,11 +720,19 @@ function isPlainObject(value) {
|
|
|
667
720
|
class MnConfigService {
|
|
668
721
|
http;
|
|
669
722
|
_config = null;
|
|
723
|
+
_settings = {};
|
|
670
724
|
_debugMode = false;
|
|
725
|
+
/** Reactive version counter — incremented on every config load. */
|
|
726
|
+
_configVersion = signal(0, ...(ngDevMode ? [{ debugName: "_configVersion" }] : []));
|
|
727
|
+
configVersion = this._configVersion.asReadonly();
|
|
671
728
|
lang = inject(MnLanguageService);
|
|
672
729
|
constructor(http) {
|
|
673
730
|
this.http = http;
|
|
674
731
|
}
|
|
732
|
+
/** General settings from the config file (version, name, etc.). */
|
|
733
|
+
get settings() {
|
|
734
|
+
return this._settings;
|
|
735
|
+
}
|
|
675
736
|
/**
|
|
676
737
|
* Load the configuration JSON from the provided URL and cache it in memory.
|
|
677
738
|
* Consumers should typically call this via the APP_INITIALIZER helper.
|
|
@@ -698,7 +759,9 @@ class MnConfigService {
|
|
|
698
759
|
const cfg = (isPlainObject(json) ? json : {});
|
|
699
760
|
const defaults = isPlainObject(cfg.defaults) ? cfg.defaults : {};
|
|
700
761
|
const overrides = isPlainObject(cfg.overrides) ? cfg.overrides : cfg.overrides ?? {};
|
|
701
|
-
|
|
762
|
+
const settings = isPlainObject(cfg.settings) ? cfg.settings : {};
|
|
763
|
+
this._config = { settings, defaults, overrides };
|
|
764
|
+
this._settings = settings;
|
|
702
765
|
// Bootstrap language service from config if a "language" section is present.
|
|
703
766
|
// This avoids circular dependency: config reads raw language settings and
|
|
704
767
|
// pushes them into the language service (language service never imports config).
|
|
@@ -711,6 +774,31 @@ class MnConfigService {
|
|
|
711
774
|
await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
|
|
712
775
|
await this.lang.setLocale(effectiveLocale);
|
|
713
776
|
}
|
|
777
|
+
this._configVersion.update(v => v + 1);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Load configuration from a pre-parsed object (no HTTP fetch).
|
|
781
|
+
* Used for live preview scenarios where config is pushed via postMessage.
|
|
782
|
+
* Optionally re-bootstraps the language service if a `language` section is present.
|
|
783
|
+
*/
|
|
784
|
+
async loadFromObject(config, bootstrapLanguage = false) {
|
|
785
|
+
const defaults = isPlainObject(config['defaults']) ? config['defaults'] : {};
|
|
786
|
+
const overrides = isPlainObject(config['overrides']) ? config['overrides'] : {};
|
|
787
|
+
const settings = isPlainObject(config['settings']) ? config['settings'] : {};
|
|
788
|
+
this._config = { settings, defaults, overrides };
|
|
789
|
+
this._settings = settings;
|
|
790
|
+
if (bootstrapLanguage) {
|
|
791
|
+
const langCfg = config['language'];
|
|
792
|
+
if (isPlainObject(langCfg) && typeof langCfg['urlPattern'] === 'string') {
|
|
793
|
+
const lc = langCfg;
|
|
794
|
+
this.lang.configure(lc.urlPattern);
|
|
795
|
+
const effectiveLocale = this.lang.resolveLocaleForDomain(lc.domainLocaleMap, lc.defaultLocale);
|
|
796
|
+
const localesToLoad = lc.preload ?? [effectiveLocale];
|
|
797
|
+
await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
|
|
798
|
+
await this.lang.setLocale(effectiveLocale);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
this._configVersion.update(v => v + 1);
|
|
714
802
|
}
|
|
715
803
|
/**
|
|
716
804
|
* Resolve a configuration object for a component, optionally scoped to a section path
|
|
@@ -865,6 +953,7 @@ class MnInputField {
|
|
|
865
953
|
ngControl;
|
|
866
954
|
/** Resolved UI configuration for the input field */
|
|
867
955
|
uiConfig = {};
|
|
956
|
+
el = inject(ElementRef);
|
|
868
957
|
/** Configuration properties for the input field */
|
|
869
958
|
props;
|
|
870
959
|
configService = inject(MnConfigService);
|
|
@@ -910,6 +999,17 @@ class MnInputField {
|
|
|
910
999
|
this.resolveConfig();
|
|
911
1000
|
});
|
|
912
1001
|
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
1002
|
+
if (this.props.autoFocus) {
|
|
1003
|
+
setTimeout(() => this.focus(), 0);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Focuses the input element.
|
|
1008
|
+
*/
|
|
1009
|
+
focus() {
|
|
1010
|
+
const input = this.el.nativeElement.querySelector('input');
|
|
1011
|
+
if (input)
|
|
1012
|
+
input.focus();
|
|
913
1013
|
}
|
|
914
1014
|
resolveConfig() {
|
|
915
1015
|
const instanceId = this.explicitInstanceId || `mn-input-${this.props.id}`;
|
|
@@ -971,8 +1071,13 @@ class MnInputField {
|
|
|
971
1071
|
* @param raw - Raw string value from the input element
|
|
972
1072
|
*/
|
|
973
1073
|
handleInput(raw) {
|
|
974
|
-
|
|
975
|
-
|
|
1074
|
+
let finalValue = raw;
|
|
1075
|
+
// Apply mask if available
|
|
1076
|
+
if (this.props.mask && typeof this.adapter.applyMask === 'function') {
|
|
1077
|
+
finalValue = this.adapter.applyMask(raw, this.props.mask);
|
|
1078
|
+
}
|
|
1079
|
+
this.value = finalValue;
|
|
1080
|
+
this.onChange(this.adapter.parse(finalValue));
|
|
976
1081
|
}
|
|
977
1082
|
/**
|
|
978
1083
|
* Handles blur events from the input element.
|
|
@@ -1127,11 +1232,11 @@ class MnInputField {
|
|
|
1127
1232
|
});
|
|
1128
1233
|
}
|
|
1129
1234
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
1130
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [
|
|
1235
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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 }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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"] }] });
|
|
1131
1236
|
}
|
|
1132
1237
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, decorators: [{
|
|
1133
1238
|
type: Component,
|
|
1134
|
-
args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [
|
|
1239
|
+
args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [CommonModule, NgClass, MnErrorMessage, FormsModule], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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 }\n</div>\n" }]
|
|
1135
1240
|
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
1136
1241
|
type: Optional
|
|
1137
1242
|
}, {
|
|
@@ -1374,6 +1479,7 @@ class MnTextarea {
|
|
|
1374
1479
|
ngControl;
|
|
1375
1480
|
/** Resolved UI configuration for the textarea */
|
|
1376
1481
|
uiConfig = {};
|
|
1482
|
+
el = inject(ElementRef);
|
|
1377
1483
|
/** Configuration properties for the textarea */
|
|
1378
1484
|
props;
|
|
1379
1485
|
configService = inject(MnConfigService);
|
|
@@ -1416,6 +1522,17 @@ class MnTextarea {
|
|
|
1416
1522
|
this.resolveConfig();
|
|
1417
1523
|
});
|
|
1418
1524
|
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
1525
|
+
if (this.props.autoFocus) {
|
|
1526
|
+
setTimeout(() => this.focus(), 0);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Focuses the textarea element.
|
|
1531
|
+
*/
|
|
1532
|
+
focus() {
|
|
1533
|
+
const textarea = this.el.nativeElement.querySelector('textarea');
|
|
1534
|
+
if (textarea)
|
|
1535
|
+
textarea.focus();
|
|
1419
1536
|
}
|
|
1420
1537
|
resolveConfig() {
|
|
1421
1538
|
const instanceId = this.explicitInstanceId || `mn-textarea-${this.props.id}`;
|
|
@@ -1528,6 +1645,10 @@ class MnTextarea {
|
|
|
1528
1645
|
if (typeof msgDef === 'function') {
|
|
1529
1646
|
return msgDef(errorArgs, errors);
|
|
1530
1647
|
}
|
|
1648
|
+
// Interpolate {{placeholder}} tokens with validation error args
|
|
1649
|
+
if (typeof msgDef === 'string' && errorArgs && typeof errorArgs === 'object') {
|
|
1650
|
+
return msgDef.replace(/\{\{(\w+)\}\}/g, (_, key) => errorArgs[key] ?? _);
|
|
1651
|
+
}
|
|
1531
1652
|
return msgDef;
|
|
1532
1653
|
}
|
|
1533
1654
|
/**
|
|
@@ -1577,11 +1698,11 @@ class MnTextarea {
|
|
|
1577
1698
|
});
|
|
1578
1699
|
}
|
|
1579
1700
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTextarea, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
1580
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTextarea, isStandalone: true, selector: "mn-lib-textarea", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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"] }] });
|
|
1701
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTextarea, isStandalone: true, selector: "mn-lib-textarea", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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"] }] });
|
|
1581
1702
|
}
|
|
1582
1703
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTextarea, decorators: [{
|
|
1583
1704
|
type: Component,
|
|
1584
|
-
args: [{ selector: 'mn-lib-textarea', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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" }]
|
|
1705
|
+
args: [{ selector: 'mn-lib-textarea', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-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" }]
|
|
1585
1706
|
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
1586
1707
|
type: Optional
|
|
1587
1708
|
}, {
|
|
@@ -2105,6 +2226,10 @@ class MnMultiSelect {
|
|
|
2105
2226
|
if (typeof msgDef === 'function') {
|
|
2106
2227
|
return msgDef(errorArgs, errors);
|
|
2107
2228
|
}
|
|
2229
|
+
// Interpolate {{placeholder}} tokens with validation error args
|
|
2230
|
+
if (typeof msgDef === 'string' && errorArgs && typeof errorArgs === 'object') {
|
|
2231
|
+
return msgDef.replace(/\{\{(\w+)\}\}/g, (_, key) => errorArgs[key] ?? _);
|
|
2232
|
+
}
|
|
2108
2233
|
return msgDef;
|
|
2109
2234
|
}
|
|
2110
2235
|
get errorMessages() {
|
|
@@ -2299,53 +2424,41 @@ var ValidationCode;
|
|
|
2299
2424
|
ValidationCode["CUSTOM"] = "custom";
|
|
2300
2425
|
})(ValidationCode || (ValidationCode = {}));
|
|
2301
2426
|
|
|
2302
|
-
|
|
2427
|
+
/**
|
|
2428
|
+
* A builder class that provides form layout capabilities (fields, rows, groups).
|
|
2429
|
+
* This can be used as a delegate to avoid code duplication between FormModalBuilder and StepBuilder.
|
|
2430
|
+
*/
|
|
2431
|
+
class FormLayoutBuilder {
|
|
2303
2432
|
config;
|
|
2433
|
+
parent;
|
|
2304
2434
|
currentRow = [];
|
|
2305
2435
|
currentRowColumns = 1;
|
|
2306
|
-
constructor(
|
|
2307
|
-
this.config =
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
state: StepState.PENDING,
|
|
2312
|
-
};
|
|
2436
|
+
constructor(config, parent) {
|
|
2437
|
+
this.config = config;
|
|
2438
|
+
this.parent = parent;
|
|
2439
|
+
this.config.fields = this.config.fields || [];
|
|
2440
|
+
this.config.rows = this.config.rows || [];
|
|
2313
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Add a custom body/content to the form/step.
|
|
2444
|
+
*/
|
|
2314
2445
|
body(body) {
|
|
2315
2446
|
this.config.body = body;
|
|
2316
|
-
return this;
|
|
2317
|
-
}
|
|
2318
|
-
state(state) {
|
|
2319
|
-
this.config.state = state;
|
|
2320
|
-
return this;
|
|
2321
|
-
}
|
|
2322
|
-
guard(guard) {
|
|
2323
|
-
this.config.guard = guard;
|
|
2324
|
-
return this;
|
|
2325
|
-
}
|
|
2326
|
-
validators(validators) {
|
|
2327
|
-
this.config.validators = validators;
|
|
2328
|
-
return this;
|
|
2447
|
+
return this.parent;
|
|
2329
2448
|
}
|
|
2330
2449
|
/**
|
|
2331
|
-
* Add a field
|
|
2332
|
-
*
|
|
2333
|
-
* @example
|
|
2334
|
-
* s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
|
|
2335
|
-
* s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
|
|
2450
|
+
* Add a field as a full-width row (single column).
|
|
2336
2451
|
*/
|
|
2337
2452
|
field(field) {
|
|
2338
2453
|
this.flushCurrentRow();
|
|
2339
2454
|
this.config.fields = this.config.fields || [];
|
|
2340
2455
|
this.config.fields.push(field);
|
|
2341
|
-
|
|
2342
|
-
this.config.rows = [];
|
|
2343
|
-
}
|
|
2456
|
+
this.config.rows = this.config.rows || [];
|
|
2344
2457
|
this.config.rows.push({
|
|
2345
2458
|
columns: 1,
|
|
2346
2459
|
fields: [{ field, span: 1 }],
|
|
2347
2460
|
});
|
|
2348
|
-
return this;
|
|
2461
|
+
return this.parent;
|
|
2349
2462
|
}
|
|
2350
2463
|
/**
|
|
2351
2464
|
* Start a new row with the specified number of columns.
|
|
@@ -2354,70 +2467,293 @@ class StepBuilder {
|
|
|
2354
2467
|
row(columns = 2) {
|
|
2355
2468
|
this.flushCurrentRow();
|
|
2356
2469
|
this.currentRowColumns = columns;
|
|
2357
|
-
return this;
|
|
2470
|
+
return this.parent;
|
|
2358
2471
|
}
|
|
2359
2472
|
/**
|
|
2360
2473
|
* Add a field to the current row started by `row()`.
|
|
2474
|
+
* @param field - The field configuration
|
|
2475
|
+
* @param span - How many columns this field should span (default: 1)
|
|
2361
2476
|
*/
|
|
2362
2477
|
addToRow(field, span = 1) {
|
|
2363
2478
|
this.config.fields = this.config.fields || [];
|
|
2364
2479
|
this.config.fields.push(field);
|
|
2365
2480
|
this.currentRow.push({ field, span });
|
|
2366
|
-
return this;
|
|
2481
|
+
return this.parent;
|
|
2367
2482
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2483
|
+
/**
|
|
2484
|
+
* Declarative way to add a row.
|
|
2485
|
+
* @example
|
|
2486
|
+
* .addRow(2, row => {
|
|
2487
|
+
* row.add({ kind: FieldKind.TEXT, key: 'first', ... });
|
|
2488
|
+
* row.add({ kind: FieldKind.TEXT, key: 'last', ... });
|
|
2489
|
+
* })
|
|
2490
|
+
*/
|
|
2491
|
+
addRow(columns, buildFn) {
|
|
2492
|
+
this.flushCurrentRow();
|
|
2493
|
+
const rowFields = [];
|
|
2494
|
+
buildFn({
|
|
2495
|
+
add: (field, span = 1) => {
|
|
2496
|
+
this.config.fields = this.config.fields || [];
|
|
2497
|
+
this.config.fields.push(field);
|
|
2498
|
+
rowFields.push({ field, span });
|
|
2499
|
+
},
|
|
2500
|
+
});
|
|
2501
|
+
if (rowFields.length > 0) {
|
|
2502
|
+
this.config.rows = this.config.rows || [];
|
|
2373
2503
|
this.config.rows.push({
|
|
2374
|
-
columns
|
|
2375
|
-
fields:
|
|
2504
|
+
columns,
|
|
2505
|
+
fields: rowFields,
|
|
2376
2506
|
});
|
|
2377
|
-
this.currentRow = [];
|
|
2378
|
-
this.currentRowColumns = 1;
|
|
2379
2507
|
}
|
|
2508
|
+
return this.parent;
|
|
2380
2509
|
}
|
|
2381
|
-
|
|
2382
|
-
* Add a field group with a section header.
|
|
2383
|
-
*/
|
|
2384
|
-
fieldGroup(group) {
|
|
2510
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2385
2511
|
this.flushCurrentRow();
|
|
2512
|
+
if (typeof arg1 !== 'string') {
|
|
2513
|
+
const group = arg1;
|
|
2514
|
+
this.processFieldGroup(group);
|
|
2515
|
+
return this.parent;
|
|
2516
|
+
}
|
|
2517
|
+
const title = arg1;
|
|
2518
|
+
let description;
|
|
2519
|
+
let buildFn;
|
|
2520
|
+
if (typeof arg2 === 'string') {
|
|
2521
|
+
description = arg2;
|
|
2522
|
+
buildFn = arg3;
|
|
2523
|
+
}
|
|
2524
|
+
else {
|
|
2525
|
+
buildFn = arg2;
|
|
2526
|
+
}
|
|
2527
|
+
const groupConfig = {
|
|
2528
|
+
fields: [],
|
|
2529
|
+
rows: [],
|
|
2530
|
+
};
|
|
2531
|
+
const groupBuilder = new FormLayoutBuilder(groupConfig, {});
|
|
2532
|
+
// Overwrite the groupBuilder's parent to itself for proper chaining within the group
|
|
2533
|
+
groupBuilder.parent = groupBuilder;
|
|
2534
|
+
buildFn(groupBuilder);
|
|
2535
|
+
groupBuilder.flushCurrentRow();
|
|
2536
|
+
const group = {
|
|
2537
|
+
title,
|
|
2538
|
+
description,
|
|
2539
|
+
fields: groupConfig.fields || [],
|
|
2540
|
+
rows: groupConfig.rows || [],
|
|
2541
|
+
};
|
|
2542
|
+
this.processFieldGroup(group);
|
|
2543
|
+
return this.parent;
|
|
2544
|
+
}
|
|
2545
|
+
processFieldGroup(group) {
|
|
2386
2546
|
if (!this.config.fieldGroups) {
|
|
2387
2547
|
this.config.fieldGroups = [];
|
|
2388
2548
|
}
|
|
2549
|
+
// Also add group fields to the flat fields array for form control creation
|
|
2389
2550
|
this.config.fields = this.config.fields || [];
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2551
|
+
const fields = this.config.fields;
|
|
2552
|
+
group.fields.forEach((f) => fields.push(f));
|
|
2553
|
+
// Build rows for the group if not provided
|
|
2554
|
+
if (!group.rows || group.rows.length === 0) {
|
|
2555
|
+
group.rows = group.fields.map((f) => ({
|
|
2393
2556
|
columns: 1,
|
|
2394
2557
|
fields: [{ field: f, span: 1 }],
|
|
2395
2558
|
}));
|
|
2396
2559
|
}
|
|
2397
2560
|
this.config.fieldGroups.push(group);
|
|
2398
|
-
return this;
|
|
2399
2561
|
}
|
|
2400
2562
|
/**
|
|
2401
|
-
* Add form-level validators for cross-field validation
|
|
2563
|
+
* Add form-level validators for cross-field validation.
|
|
2402
2564
|
*/
|
|
2403
2565
|
formValidators(validators) {
|
|
2404
2566
|
this.config.formValidators = validators;
|
|
2405
|
-
return this;
|
|
2567
|
+
return this.parent;
|
|
2406
2568
|
}
|
|
2407
2569
|
/**
|
|
2408
|
-
* Add Angular FormGroup-level validators
|
|
2570
|
+
* Add Angular FormGroup-level validators.
|
|
2409
2571
|
*/
|
|
2410
2572
|
groupValidators(validators) {
|
|
2411
2573
|
this.config.groupValidators = validators;
|
|
2412
|
-
return this;
|
|
2574
|
+
return this.parent;
|
|
2413
2575
|
}
|
|
2414
2576
|
/**
|
|
2415
|
-
* Set initial
|
|
2577
|
+
* Set initial value for fields.
|
|
2416
2578
|
*/
|
|
2417
2579
|
initialValue(value) {
|
|
2418
2580
|
this.config.initialValue = value;
|
|
2581
|
+
return this.parent;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Set the field to be focused when the form initializes.
|
|
2585
|
+
*/
|
|
2586
|
+
focus(key) {
|
|
2587
|
+
// Clear autoFocus from other fields first to ensure only one is focused
|
|
2588
|
+
this.config.fields?.forEach(f => {
|
|
2589
|
+
f.autoFocus = false;
|
|
2590
|
+
});
|
|
2591
|
+
this.config.fieldGroups?.forEach(g => {
|
|
2592
|
+
g.fields.forEach(f => {
|
|
2593
|
+
f.autoFocus = false;
|
|
2594
|
+
});
|
|
2595
|
+
});
|
|
2596
|
+
const field = this.config.fields?.find(f => f.key === key);
|
|
2597
|
+
if (field) {
|
|
2598
|
+
field.autoFocus = true;
|
|
2599
|
+
}
|
|
2600
|
+
return this.parent;
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* Wraps a field with a fluent API for validation.
|
|
2604
|
+
*/
|
|
2605
|
+
fieldWithValidators(field) {
|
|
2606
|
+
const fieldAny = field;
|
|
2607
|
+
fieldAny.validators = fieldAny.validators || [];
|
|
2608
|
+
this.field(field);
|
|
2609
|
+
return new FieldValidatorBuilder(field, this.parent);
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Flushes any pending fields in the current row to the configuration.
|
|
2613
|
+
*/
|
|
2614
|
+
flushCurrentRow() {
|
|
2615
|
+
if (this.currentRow.length > 0) {
|
|
2616
|
+
this.config.rows.push({
|
|
2617
|
+
columns: this.currentRowColumns,
|
|
2618
|
+
fields: [...this.currentRow],
|
|
2619
|
+
});
|
|
2620
|
+
this.currentRow = [];
|
|
2621
|
+
this.currentRowColumns = 1;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* A builder for adding validation rules to a field fluently.
|
|
2627
|
+
*/
|
|
2628
|
+
class FieldValidatorBuilder {
|
|
2629
|
+
field;
|
|
2630
|
+
parent;
|
|
2631
|
+
constructor(field, parent) {
|
|
2632
|
+
this.field = field;
|
|
2633
|
+
this.parent = parent;
|
|
2634
|
+
this.field.validators = this.field.validators || [];
|
|
2635
|
+
}
|
|
2636
|
+
required(message) {
|
|
2637
|
+
this.field.validators.push(Validators.required);
|
|
2638
|
+
return this;
|
|
2639
|
+
}
|
|
2640
|
+
minLength(length) {
|
|
2641
|
+
this.field.validators.push(Validators.minLength(length));
|
|
2642
|
+
return this;
|
|
2643
|
+
}
|
|
2644
|
+
maxLength(length) {
|
|
2645
|
+
this.field.validators.push(Validators.maxLength(length));
|
|
2646
|
+
return this;
|
|
2647
|
+
}
|
|
2648
|
+
pattern(pattern) {
|
|
2649
|
+
this.field.validators.push(Validators.pattern(pattern));
|
|
2650
|
+
return this;
|
|
2651
|
+
}
|
|
2652
|
+
email() {
|
|
2653
|
+
this.field.validators.push(Validators.email);
|
|
2654
|
+
return this;
|
|
2655
|
+
}
|
|
2656
|
+
min(value) {
|
|
2657
|
+
this.field.validators.push(Validators.min(value));
|
|
2658
|
+
return this;
|
|
2659
|
+
}
|
|
2660
|
+
max(value) {
|
|
2661
|
+
this.field.validators.push(Validators.max(value));
|
|
2662
|
+
return this;
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Add a custom validator.
|
|
2666
|
+
*/
|
|
2667
|
+
custom(validator) {
|
|
2668
|
+
this.field.validators.push(validator);
|
|
2669
|
+
return this;
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Return to the parent builder.
|
|
2673
|
+
*/
|
|
2674
|
+
done() {
|
|
2675
|
+
return this.parent;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
class StepBuilder {
|
|
2680
|
+
config;
|
|
2681
|
+
layoutBuilder;
|
|
2682
|
+
constructor(id, title) {
|
|
2683
|
+
this.config = {
|
|
2684
|
+
id,
|
|
2685
|
+
title,
|
|
2686
|
+
fields: [],
|
|
2687
|
+
state: StepState.PENDING,
|
|
2688
|
+
};
|
|
2689
|
+
this.layoutBuilder = new FormLayoutBuilder(this.config, this);
|
|
2690
|
+
}
|
|
2691
|
+
body(body) {
|
|
2692
|
+
this.config.body = body;
|
|
2693
|
+
return this;
|
|
2694
|
+
}
|
|
2695
|
+
state(state) {
|
|
2696
|
+
this.config.state = state;
|
|
2697
|
+
return this;
|
|
2698
|
+
}
|
|
2699
|
+
guard(guard) {
|
|
2700
|
+
this.config.guard = guard;
|
|
2701
|
+
return this;
|
|
2702
|
+
}
|
|
2703
|
+
validators(validators) {
|
|
2704
|
+
this.config.validators = validators;
|
|
2419
2705
|
return this;
|
|
2420
2706
|
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Add a field to this step. This is the single API for all field types.
|
|
2709
|
+
*
|
|
2710
|
+
* @example
|
|
2711
|
+
* s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
|
|
2712
|
+
* s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
|
|
2713
|
+
*/
|
|
2714
|
+
field(field) {
|
|
2715
|
+
return this.layoutBuilder.field(field);
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Start a new row with the specified number of columns.
|
|
2719
|
+
* All subsequent `addToRow()` calls will add fields to this row.
|
|
2720
|
+
*/
|
|
2721
|
+
row(columns = 2) {
|
|
2722
|
+
return this.layoutBuilder.row(columns);
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Add a field to the current row started by `row()`.
|
|
2726
|
+
*/
|
|
2727
|
+
addToRow(field, span = 1) {
|
|
2728
|
+
return this.layoutBuilder.addToRow(field, span);
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Declarative way to add a row.
|
|
2732
|
+
*/
|
|
2733
|
+
addRow(columns, buildFn) {
|
|
2734
|
+
return this.layoutBuilder.addRow(columns, buildFn);
|
|
2735
|
+
}
|
|
2736
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2737
|
+
return this.layoutBuilder.fieldGroup(arg1, arg2, arg3);
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* Add form-level validators for cross-field validation within this step.
|
|
2741
|
+
*/
|
|
2742
|
+
formValidators(validators) {
|
|
2743
|
+
return this.layoutBuilder.formValidators(validators);
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Add Angular FormGroup-level validators for this step.
|
|
2747
|
+
*/
|
|
2748
|
+
groupValidators(validators) {
|
|
2749
|
+
return this.layoutBuilder.groupValidators(validators);
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Set initial values for fields in this step.
|
|
2753
|
+
*/
|
|
2754
|
+
initialValue(value) {
|
|
2755
|
+
return this.layoutBuilder.initialValue(value);
|
|
2756
|
+
}
|
|
2421
2757
|
/**
|
|
2422
2758
|
* Set a visibility condition for this step based on aggregated wizard data.
|
|
2423
2759
|
*/
|
|
@@ -2425,16 +2761,40 @@ class StepBuilder {
|
|
|
2425
2761
|
this.config.visible = condition;
|
|
2426
2762
|
return this;
|
|
2427
2763
|
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Set a custom label for the 'Next' button on this step.
|
|
2766
|
+
*/
|
|
2767
|
+
nextLabel(label) {
|
|
2768
|
+
this.config.nextLabel = label;
|
|
2769
|
+
return this;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Set a custom label for the 'Back' button on this step.
|
|
2773
|
+
*/
|
|
2774
|
+
backLabel(label) {
|
|
2775
|
+
this.config.backLabel = label;
|
|
2776
|
+
return this;
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Hide the 'Back' button on this step.
|
|
2780
|
+
*/
|
|
2781
|
+
hideBack(hide = true) {
|
|
2782
|
+
this.config.hideBack = hide;
|
|
2783
|
+
return this;
|
|
2784
|
+
}
|
|
2428
2785
|
build() {
|
|
2429
|
-
this.flushCurrentRow();
|
|
2786
|
+
this.layoutBuilder.flushCurrentRow();
|
|
2430
2787
|
return this.config;
|
|
2431
2788
|
}
|
|
2432
2789
|
}
|
|
2433
2790
|
|
|
2434
2791
|
class BaseModalBuilder {
|
|
2435
2792
|
config;
|
|
2793
|
+
layoutBuilder;
|
|
2436
2794
|
constructor(initialConfig) {
|
|
2437
2795
|
this.config = initialConfig;
|
|
2796
|
+
// Base layout builder, we pass this.config directly if it supports it
|
|
2797
|
+
this.layoutBuilder = new FormLayoutBuilder(this.config, this);
|
|
2438
2798
|
}
|
|
2439
2799
|
title(title) {
|
|
2440
2800
|
this.config.title = title;
|
|
@@ -2472,6 +2832,14 @@ class BaseModalBuilder {
|
|
|
2472
2832
|
this.config.intent = intent;
|
|
2473
2833
|
return this;
|
|
2474
2834
|
}
|
|
2835
|
+
readOnly(readOnly = true) {
|
|
2836
|
+
this.config.readOnly = readOnly;
|
|
2837
|
+
return this;
|
|
2838
|
+
}
|
|
2839
|
+
disabled(disabled = true) {
|
|
2840
|
+
this.config.disabled = disabled;
|
|
2841
|
+
return this;
|
|
2842
|
+
}
|
|
2475
2843
|
footerActions(actions) {
|
|
2476
2844
|
this.config.footerActions = actions;
|
|
2477
2845
|
return this;
|
|
@@ -2488,7 +2856,63 @@ class BaseModalBuilder {
|
|
|
2488
2856
|
this.config.i18n = labels;
|
|
2489
2857
|
return this;
|
|
2490
2858
|
}
|
|
2859
|
+
component(component) {
|
|
2860
|
+
this.config.component = component;
|
|
2861
|
+
return this;
|
|
2862
|
+
}
|
|
2863
|
+
template(template) {
|
|
2864
|
+
this.config.template = template;
|
|
2865
|
+
return this;
|
|
2866
|
+
}
|
|
2867
|
+
inputs(inputs) {
|
|
2868
|
+
this.config.inputs = inputs;
|
|
2869
|
+
return this;
|
|
2870
|
+
}
|
|
2871
|
+
animation(animation) {
|
|
2872
|
+
this.config.animation = animation;
|
|
2873
|
+
return this;
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Add a custom body/content to the modal.
|
|
2877
|
+
*/
|
|
2878
|
+
body(body) {
|
|
2879
|
+
return this.layoutBuilder.body(body);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Add a field.
|
|
2883
|
+
*/
|
|
2884
|
+
field(field) {
|
|
2885
|
+
return this.layoutBuilder.field(field);
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Add a field with a fluent validation builder.
|
|
2889
|
+
*/
|
|
2890
|
+
fieldWithValidators(field) {
|
|
2891
|
+
return this.layoutBuilder.fieldWithValidators(field);
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Start a new row.
|
|
2895
|
+
*/
|
|
2896
|
+
row(columns = 2) {
|
|
2897
|
+
return this.layoutBuilder.row(columns);
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Add a field to the current row.
|
|
2901
|
+
*/
|
|
2902
|
+
addToRow(field, span = 1) {
|
|
2903
|
+
return this.layoutBuilder.addToRow(field, span);
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Declarative way to add a row.
|
|
2907
|
+
*/
|
|
2908
|
+
addRow(columns, buildFn) {
|
|
2909
|
+
return this.layoutBuilder.addRow(columns, buildFn);
|
|
2910
|
+
}
|
|
2911
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2912
|
+
return this.layoutBuilder.fieldGroup(arg1, arg2, arg3);
|
|
2913
|
+
}
|
|
2491
2914
|
build() {
|
|
2915
|
+
this.layoutBuilder.flushCurrentRow();
|
|
2492
2916
|
return Object.freeze({ ...this.config });
|
|
2493
2917
|
}
|
|
2494
2918
|
}
|
|
@@ -2500,6 +2924,32 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2500
2924
|
steps: [],
|
|
2501
2925
|
});
|
|
2502
2926
|
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Set initial values for the entire wizard.
|
|
2929
|
+
* Note: This will be merged with individual step initial values.
|
|
2930
|
+
*/
|
|
2931
|
+
initialValue(value) {
|
|
2932
|
+
this.config.initialValue = value;
|
|
2933
|
+
return this;
|
|
2934
|
+
}
|
|
2935
|
+
body(body) {
|
|
2936
|
+
return super.body(body);
|
|
2937
|
+
}
|
|
2938
|
+
field(field) {
|
|
2939
|
+
return super.field(field);
|
|
2940
|
+
}
|
|
2941
|
+
row(columns = 2) {
|
|
2942
|
+
return super.row(columns);
|
|
2943
|
+
}
|
|
2944
|
+
addToRow(field, span = 1) {
|
|
2945
|
+
return super.addToRow(field, span);
|
|
2946
|
+
}
|
|
2947
|
+
addRow(columns, buildFn) {
|
|
2948
|
+
return super.addRow(columns, buildFn);
|
|
2949
|
+
}
|
|
2950
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2951
|
+
return super.fieldGroup(arg1, arg2, arg3);
|
|
2952
|
+
}
|
|
2503
2953
|
step(step) {
|
|
2504
2954
|
if (typeof step === 'function') {
|
|
2505
2955
|
const builder = new StepBuilder('', '');
|
|
@@ -2516,7 +2966,8 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2516
2966
|
const stepId = id || `step-${this.config.steps.length}`;
|
|
2517
2967
|
const builder = new StepBuilder(stepId, title);
|
|
2518
2968
|
buildFn(builder);
|
|
2519
|
-
|
|
2969
|
+
const builtStep = builder.build();
|
|
2970
|
+
this.config.steps.push(builtStep);
|
|
2520
2971
|
return this;
|
|
2521
2972
|
}
|
|
2522
2973
|
startAt(stepId) {
|
|
@@ -2542,8 +2993,6 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2542
2993
|
}
|
|
2543
2994
|
|
|
2544
2995
|
class FormModalBuilder extends BaseModalBuilder {
|
|
2545
|
-
currentRow = [];
|
|
2546
|
-
currentRowColumns = 1;
|
|
2547
2996
|
constructor() {
|
|
2548
2997
|
super({
|
|
2549
2998
|
kind: ModalKind.FORM,
|
|
@@ -2551,64 +3000,27 @@ class FormModalBuilder extends BaseModalBuilder {
|
|
|
2551
3000
|
rows: [],
|
|
2552
3001
|
});
|
|
2553
3002
|
}
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
this.config.fields.push(field);
|
|
2561
|
-
this.config.rows.push({
|
|
2562
|
-
columns: 1,
|
|
2563
|
-
fields: [{ field, span: 1 }],
|
|
2564
|
-
});
|
|
2565
|
-
return this;
|
|
2566
|
-
}
|
|
2567
|
-
/**
|
|
2568
|
-
* Start a new row with the specified number of columns.
|
|
2569
|
-
* All subsequent `addToRow()` calls will add fields to this row
|
|
2570
|
-
* until the next `row()` or `field()` call.
|
|
2571
|
-
*
|
|
2572
|
-
* @example
|
|
2573
|
-
* .row(2)
|
|
2574
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'firstName', label: 'First Name' })
|
|
2575
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'lastName', label: 'Last Name' })
|
|
2576
|
-
* .row(3)
|
|
2577
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'city', label: 'City' }, 2)
|
|
2578
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'zip', label: 'ZIP' })
|
|
2579
|
-
*/
|
|
3003
|
+
body(body) {
|
|
3004
|
+
return super.body(body);
|
|
3005
|
+
}
|
|
3006
|
+
field(field) {
|
|
3007
|
+
return super.field(field);
|
|
3008
|
+
}
|
|
2580
3009
|
row(columns = 2) {
|
|
2581
|
-
|
|
2582
|
-
this.currentRowColumns = columns;
|
|
2583
|
-
return this;
|
|
3010
|
+
return super.row(columns);
|
|
2584
3011
|
}
|
|
2585
|
-
/**
|
|
2586
|
-
* Add a field to the current row started by `row()`.
|
|
2587
|
-
* @param field - The field configuration
|
|
2588
|
-
* @param span - How many columns this field should span (default: 1)
|
|
2589
|
-
*/
|
|
2590
3012
|
addToRow(field, span = 1) {
|
|
2591
|
-
|
|
2592
|
-
this.currentRow.push({ field, span });
|
|
2593
|
-
return this;
|
|
3013
|
+
return super.addToRow(field, span);
|
|
2594
3014
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
this.config.rows.push({
|
|
2598
|
-
columns: this.currentRowColumns,
|
|
2599
|
-
fields: [...this.currentRow],
|
|
2600
|
-
});
|
|
2601
|
-
this.currentRow = [];
|
|
2602
|
-
this.currentRowColumns = 1;
|
|
2603
|
-
}
|
|
3015
|
+
addRow(columns, buildFn) {
|
|
3016
|
+
return super.addRow(columns, buildFn);
|
|
2604
3017
|
}
|
|
2605
3018
|
layout(mode) {
|
|
2606
3019
|
this.config.layout = mode;
|
|
2607
3020
|
return this;
|
|
2608
3021
|
}
|
|
2609
3022
|
initialValue(value) {
|
|
2610
|
-
this.
|
|
2611
|
-
return this;
|
|
3023
|
+
return this.layoutBuilder.initialValue(value);
|
|
2612
3024
|
}
|
|
2613
3025
|
submitMode(mode) {
|
|
2614
3026
|
this.config.submitMode = mode;
|
|
@@ -2618,45 +3030,16 @@ class FormModalBuilder extends BaseModalBuilder {
|
|
|
2618
3030
|
this.config.onComplete = handler;
|
|
2619
3031
|
return this;
|
|
2620
3032
|
}
|
|
2621
|
-
/**
|
|
2622
|
-
* Add form-level validators for cross-field validation.
|
|
2623
|
-
* These receive the entire form value and return an error map or null.
|
|
2624
|
-
*/
|
|
2625
3033
|
formValidators(validators) {
|
|
2626
|
-
this.
|
|
2627
|
-
return this;
|
|
3034
|
+
return this.layoutBuilder.formValidators(validators);
|
|
2628
3035
|
}
|
|
2629
|
-
/**
|
|
2630
|
-
* Add Angular FormGroup-level validators.
|
|
2631
|
-
* These are standard Angular ValidatorFn applied to the FormGroup itself.
|
|
2632
|
-
*/
|
|
2633
3036
|
groupValidators(validators) {
|
|
2634
|
-
this.
|
|
2635
|
-
return this;
|
|
3037
|
+
return this.layoutBuilder.groupValidators(validators);
|
|
2636
3038
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
* Groups visually separate fields with a title and optional description.
|
|
2640
|
-
*/
|
|
2641
|
-
fieldGroup(group) {
|
|
2642
|
-
this.flushCurrentRow();
|
|
2643
|
-
if (!this.config.fieldGroups) {
|
|
2644
|
-
this.config.fieldGroups = [];
|
|
2645
|
-
}
|
|
2646
|
-
// Also add group fields to the flat fields array for form control creation
|
|
2647
|
-
group.fields.forEach(f => this.config.fields.push(f));
|
|
2648
|
-
// Build rows for the group if not provided
|
|
2649
|
-
if (!group.rows) {
|
|
2650
|
-
group.rows = group.fields.map(f => ({
|
|
2651
|
-
columns: 1,
|
|
2652
|
-
fields: [{ field: f, span: 1 }],
|
|
2653
|
-
}));
|
|
2654
|
-
}
|
|
2655
|
-
this.config.fieldGroups.push(group);
|
|
2656
|
-
return this;
|
|
3039
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
3040
|
+
return super.fieldGroup(arg1, arg2, arg3);
|
|
2657
3041
|
}
|
|
2658
3042
|
build() {
|
|
2659
|
-
this.flushCurrentRow();
|
|
2660
3043
|
return super.build();
|
|
2661
3044
|
}
|
|
2662
3045
|
}
|
|
@@ -2666,6 +3049,8 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
|
|
|
2666
3049
|
super({
|
|
2667
3050
|
kind: ModalKind.CONFIRMATION,
|
|
2668
3051
|
message: '',
|
|
3052
|
+
fields: [],
|
|
3053
|
+
rows: [],
|
|
2669
3054
|
});
|
|
2670
3055
|
}
|
|
2671
3056
|
message(text) {
|
|
@@ -2684,6 +3069,36 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
|
|
|
2684
3069
|
this.config.cancel = action;
|
|
2685
3070
|
return this;
|
|
2686
3071
|
}
|
|
3072
|
+
body(body) {
|
|
3073
|
+
return super.body(body);
|
|
3074
|
+
}
|
|
3075
|
+
field(field) {
|
|
3076
|
+
return super.field(field);
|
|
3077
|
+
}
|
|
3078
|
+
row(columns = 2) {
|
|
3079
|
+
return super.row(columns);
|
|
3080
|
+
}
|
|
3081
|
+
addToRow(field, span = 1) {
|
|
3082
|
+
return super.addToRow(field, span);
|
|
3083
|
+
}
|
|
3084
|
+
addRow(columns, buildFn) {
|
|
3085
|
+
return super.addRow(columns, buildFn);
|
|
3086
|
+
}
|
|
3087
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
3088
|
+
return super.fieldGroup(arg1, arg2, arg3);
|
|
3089
|
+
}
|
|
3090
|
+
initialValue(value) {
|
|
3091
|
+
return this.layoutBuilder.initialValue(value);
|
|
3092
|
+
}
|
|
3093
|
+
formValidators(validators) {
|
|
3094
|
+
return this.layoutBuilder.formValidators(validators);
|
|
3095
|
+
}
|
|
3096
|
+
groupValidators(validators) {
|
|
3097
|
+
return this.layoutBuilder.groupValidators(validators);
|
|
3098
|
+
}
|
|
3099
|
+
build() {
|
|
3100
|
+
return super.build();
|
|
3101
|
+
}
|
|
2687
3102
|
}
|
|
2688
3103
|
|
|
2689
3104
|
class CustomModalBuilder extends BaseModalBuilder {
|
|
@@ -2692,18 +3107,6 @@ class CustomModalBuilder extends BaseModalBuilder {
|
|
|
2692
3107
|
kind: ModalKind.CUSTOM,
|
|
2693
3108
|
});
|
|
2694
3109
|
}
|
|
2695
|
-
component(component) {
|
|
2696
|
-
this.config.component = component;
|
|
2697
|
-
return this;
|
|
2698
|
-
}
|
|
2699
|
-
template(template) {
|
|
2700
|
-
this.config.template = template;
|
|
2701
|
-
return this;
|
|
2702
|
-
}
|
|
2703
|
-
inputs(inputs) {
|
|
2704
|
-
this.config.inputs = inputs;
|
|
2705
|
-
return this;
|
|
2706
|
-
}
|
|
2707
3110
|
onComplete(handler) {
|
|
2708
3111
|
this.config.onComplete = handler;
|
|
2709
3112
|
return this;
|
|
@@ -2761,6 +3164,9 @@ class MnModalRef {
|
|
|
2761
3164
|
// Trigger change detection on the shell component
|
|
2762
3165
|
this.componentRef.changeDetectorRef.detectChanges();
|
|
2763
3166
|
}
|
|
3167
|
+
get component() {
|
|
3168
|
+
return this.componentRef.instance;
|
|
3169
|
+
}
|
|
2764
3170
|
destroy() {
|
|
2765
3171
|
this.componentRef.destroy();
|
|
2766
3172
|
}
|
|
@@ -3083,11 +3489,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3083
3489
|
type: Output
|
|
3084
3490
|
}] } });
|
|
3085
3491
|
|
|
3492
|
+
class MnCustomBodyHostComponent {
|
|
3493
|
+
config;
|
|
3494
|
+
modalRef;
|
|
3495
|
+
container;
|
|
3496
|
+
componentRef;
|
|
3497
|
+
ngOnInit() {
|
|
3498
|
+
setTimeout(() => this.loadContent(), 0);
|
|
3499
|
+
}
|
|
3500
|
+
loadContent() {
|
|
3501
|
+
if (!this.container)
|
|
3502
|
+
return;
|
|
3503
|
+
this.container.clear();
|
|
3504
|
+
if (this.config.component) {
|
|
3505
|
+
this.attachComponent(this.config.component);
|
|
3506
|
+
}
|
|
3507
|
+
else if (this.config.template) {
|
|
3508
|
+
this.attachTemplate(this.config.template);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
attachComponent(component) {
|
|
3512
|
+
this.componentRef = this.container.createComponent(component);
|
|
3513
|
+
// Pass inputs to the component
|
|
3514
|
+
if (this.config.inputs) {
|
|
3515
|
+
Object.entries(this.config.inputs).forEach(([key, value]) => {
|
|
3516
|
+
this.componentRef.instance[key] = value;
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
// Pass modalRef if the component has a modalRef property
|
|
3520
|
+
const instance = this.componentRef.instance;
|
|
3521
|
+
if (instance && 'modalRef' in instance) {
|
|
3522
|
+
instance.modalRef = this.modalRef;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
attachTemplate(template) {
|
|
3526
|
+
this.container.createEmbeddedView(template, {
|
|
3527
|
+
$implicit: this.modalRef,
|
|
3528
|
+
modalRef: this.modalRef,
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3532
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnCustomBodyHostComponent, isStandalone: true, selector: "mn-custom-body-host", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: '<ng-container #container></ng-container>', isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
3533
|
+
}
|
|
3534
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, decorators: [{
|
|
3535
|
+
type: Component,
|
|
3536
|
+
args: [{
|
|
3537
|
+
selector: 'mn-custom-body-host',
|
|
3538
|
+
standalone: true,
|
|
3539
|
+
imports: [CommonModule],
|
|
3540
|
+
template: '<ng-container #container></ng-container>',
|
|
3541
|
+
}]
|
|
3542
|
+
}], propDecorators: { config: [{
|
|
3543
|
+
type: Input
|
|
3544
|
+
}], modalRef: [{
|
|
3545
|
+
type: Input
|
|
3546
|
+
}], container: [{
|
|
3547
|
+
type: ViewChild,
|
|
3548
|
+
args: ['container', { read: ViewContainerRef }]
|
|
3549
|
+
}] } });
|
|
3550
|
+
|
|
3086
3551
|
class MnFormBodyComponent {
|
|
3087
3552
|
fb;
|
|
3088
3553
|
config;
|
|
3089
3554
|
modalRef;
|
|
3090
3555
|
hideFooter = false;
|
|
3556
|
+
hideCustomBody = false;
|
|
3557
|
+
formStatusChange = new EventEmitter();
|
|
3558
|
+
inputFields;
|
|
3559
|
+
textareas;
|
|
3091
3560
|
form;
|
|
3092
3561
|
rows = [];
|
|
3093
3562
|
fieldGroups = [];
|
|
@@ -3146,6 +3615,44 @@ class MnFormBodyComponent {
|
|
|
3146
3615
|
this.initializeDataSources();
|
|
3147
3616
|
this.initializeTableFields();
|
|
3148
3617
|
this.subscribeToValueChanges();
|
|
3618
|
+
if (this.config.disabled || this.config.readOnly) {
|
|
3619
|
+
this.form.disable();
|
|
3620
|
+
}
|
|
3621
|
+
// Emit initial status
|
|
3622
|
+
setTimeout(() => {
|
|
3623
|
+
this.formStatusChange.emit(this.form.status);
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
ngAfterViewInit() {
|
|
3627
|
+
// Small delay to ensure children are fully rendered and MnModalShell hasn't just stolen focus
|
|
3628
|
+
setTimeout(() => {
|
|
3629
|
+
this.applyAutoFocus();
|
|
3630
|
+
}, 100);
|
|
3631
|
+
}
|
|
3632
|
+
applyAutoFocus() {
|
|
3633
|
+
const autoFocusField = this.config.fields.find(f => f.autoFocus);
|
|
3634
|
+
if (!autoFocusField)
|
|
3635
|
+
return;
|
|
3636
|
+
const key = autoFocusField.key;
|
|
3637
|
+
// Small delay to ensure browser is ready to focus
|
|
3638
|
+
setTimeout(() => {
|
|
3639
|
+
// Try finding in MnInputField components
|
|
3640
|
+
const inputField = this.inputFields?.find(f => f.props.id === key);
|
|
3641
|
+
if (inputField) {
|
|
3642
|
+
inputField.focus();
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
// Try finding in MnTextarea components
|
|
3646
|
+
const textarea = this.textareas?.find(f => f.props?.id === key);
|
|
3647
|
+
if (textarea && typeof textarea.focus === 'function') {
|
|
3648
|
+
textarea.focus();
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
// Fallback to native element if possible
|
|
3652
|
+
const el = document.getElementById(key);
|
|
3653
|
+
if (el)
|
|
3654
|
+
el.focus();
|
|
3655
|
+
}, 50);
|
|
3149
3656
|
}
|
|
3150
3657
|
ngOnDestroy() {
|
|
3151
3658
|
this.valueChangesSubscription?.unsubscribe();
|
|
@@ -3161,7 +3668,15 @@ class MnFormBodyComponent {
|
|
|
3161
3668
|
}
|
|
3162
3669
|
const validators = fieldConfig.validators || [];
|
|
3163
3670
|
const asyncValidators = fieldConfig.asyncValidators || [];
|
|
3164
|
-
|
|
3671
|
+
const updateOn = fieldConfig.updateOn || 'change';
|
|
3672
|
+
formControls[field.key] = [
|
|
3673
|
+
initialValue,
|
|
3674
|
+
{
|
|
3675
|
+
validators,
|
|
3676
|
+
asyncValidators,
|
|
3677
|
+
updateOn
|
|
3678
|
+
}
|
|
3679
|
+
];
|
|
3165
3680
|
});
|
|
3166
3681
|
this.form = this.fb.group(formControls);
|
|
3167
3682
|
// Apply Angular FormGroup-level validators
|
|
@@ -3179,10 +3694,10 @@ class MnFormBodyComponent {
|
|
|
3179
3694
|
});
|
|
3180
3695
|
}
|
|
3181
3696
|
isFieldReadOnly(field) {
|
|
3182
|
-
return field.readOnly === true;
|
|
3697
|
+
return this.config.readOnly === true || field.readOnly === true;
|
|
3183
3698
|
}
|
|
3184
3699
|
isFieldDisabled(field) {
|
|
3185
|
-
return field.disabled === true;
|
|
3700
|
+
return this.config.disabled === true || field.disabled === true;
|
|
3186
3701
|
}
|
|
3187
3702
|
/** Track which field groups are currently visible */
|
|
3188
3703
|
groupVisibility = {};
|
|
@@ -3190,12 +3705,25 @@ class MnFormBodyComponent {
|
|
|
3190
3705
|
if (this.config.rows && this.config.rows.length > 0) {
|
|
3191
3706
|
this.rows = this.config.rows;
|
|
3192
3707
|
}
|
|
3193
|
-
else
|
|
3194
|
-
//
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3708
|
+
else {
|
|
3709
|
+
// Create rows for fields that are not in any row or group
|
|
3710
|
+
const fieldsInGroups = new Set();
|
|
3711
|
+
(this.config.fieldGroups || []).forEach(g => {
|
|
3712
|
+
(g.rows || []).forEach(r => r.fields.forEach(f => fieldsInGroups.add(f.field.key)));
|
|
3713
|
+
// Also check if group has flat fields list (compatibility)
|
|
3714
|
+
if (g.fields) {
|
|
3715
|
+
g.fields.forEach((f) => fieldsInGroups.add(f.key));
|
|
3716
|
+
}
|
|
3717
|
+
});
|
|
3718
|
+
const fieldsInRows = new Set();
|
|
3719
|
+
(this.config.rows || []).forEach(r => r.fields.forEach(f => fieldsInRows.add(f.field.key)));
|
|
3720
|
+
const standaloneFields = this.config.fields.filter(f => !fieldsInGroups.has(f.key) && !fieldsInRows.has(f.key));
|
|
3721
|
+
if (standaloneFields.length > 0) {
|
|
3722
|
+
this.rows = standaloneFields.map(field => ({
|
|
3723
|
+
columns: 1,
|
|
3724
|
+
fields: [{ field, span: 1 }],
|
|
3725
|
+
}));
|
|
3726
|
+
}
|
|
3199
3727
|
}
|
|
3200
3728
|
// Build field groups
|
|
3201
3729
|
this.fieldGroups = this.config.fieldGroups || [];
|
|
@@ -3474,6 +4002,8 @@ class MnFormBodyComponent {
|
|
|
3474
4002
|
this.runFormValidators();
|
|
3475
4003
|
// Reload data sources that depend on changed fields
|
|
3476
4004
|
this.reloadDependentDataSources(formValue);
|
|
4005
|
+
// Emit status change
|
|
4006
|
+
this.formStatusChange.emit(this.form.status);
|
|
3477
4007
|
});
|
|
3478
4008
|
}
|
|
3479
4009
|
previousFormValue = {};
|
|
@@ -3593,17 +4123,27 @@ class MnFormBodyComponent {
|
|
|
3593
4123
|
}
|
|
3594
4124
|
}
|
|
3595
4125
|
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 });
|
|
3596
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter" }, ngImport: i0, template: "<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 <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-300 rounded-xl text-base text-gray-900 bg-white 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))\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\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 })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\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 })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\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 rows: asField(rowField.field).rows || 4,\n resize: 'vertical'\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\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 })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-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-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-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 </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\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-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-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 <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-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-gray-400\" 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-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\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 <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"fieldGroups.length === 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"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</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.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { 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"] }] });
|
|
4126
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.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: "<mn-custom-body-host\n *ngIf=\"(config.component || config.template) && !hideCustomBody\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n></mn-custom-body-host>\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 <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\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 <div *ngSwitchCase=\"FieldKind.NUMBER\">\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 <div *ngSwitchCase=\"FieldKind.PASSWORD\">\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 <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-300 rounded-xl text-base text-gray-900 bg-white 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 <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\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 <div *ngSwitchCase=\"FieldKind.DATE\">\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 <div *ngSwitchCase=\"FieldKind.DATETIME\">\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 <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\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 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 <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\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 <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-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-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-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 </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\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-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-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 <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-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-gray-400\" 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-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\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 <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"rows.length > 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"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</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.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { 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"] }] });
|
|
3597
4127
|
}
|
|
3598
4128
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
|
|
3599
4129
|
type: Component,
|
|
3600
|
-
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnCustomFieldHostDirective, MnTable], template: "<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 <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-300 rounded-xl text-base text-gray-900 bg-white 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))\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\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 })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\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 })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\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 })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\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 rows: asField(rowField.field).rows || 4,\n resize: 'vertical'\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\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 })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-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-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-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 </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\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-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-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 <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-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-gray-400\" 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-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\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 <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"fieldGroups.length === 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"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</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"] }]
|
|
4130
|
+
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnCustomFieldHostDirective, MnTable, MnCustomBodyHostComponent], template: "<mn-custom-body-host\n *ngIf=\"(config.component || config.template) && !hideCustomBody\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n></mn-custom-body-host>\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 <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\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 <div *ngSwitchCase=\"FieldKind.NUMBER\">\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 <div *ngSwitchCase=\"FieldKind.PASSWORD\">\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 <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-300 rounded-xl text-base text-gray-900 bg-white 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 <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\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 <div *ngSwitchCase=\"FieldKind.DATE\">\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 <div *ngSwitchCase=\"FieldKind.DATETIME\">\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 <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\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 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 <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\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 <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\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-gray-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-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-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 </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\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-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-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 <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-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-gray-400\" 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-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\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 <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"rows.length > 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" 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 </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"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</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"] }]
|
|
3601
4131
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
|
|
3602
4132
|
type: Input
|
|
3603
4133
|
}], modalRef: [{
|
|
3604
4134
|
type: Input
|
|
3605
4135
|
}], hideFooter: [{
|
|
3606
4136
|
type: Input
|
|
4137
|
+
}], hideCustomBody: [{
|
|
4138
|
+
type: Input
|
|
4139
|
+
}], formStatusChange: [{
|
|
4140
|
+
type: Output
|
|
4141
|
+
}], inputFields: [{
|
|
4142
|
+
type: ViewChildren,
|
|
4143
|
+
args: [MnInputField]
|
|
4144
|
+
}], textareas: [{
|
|
4145
|
+
type: ViewChildren,
|
|
4146
|
+
args: [MnTextarea]
|
|
3607
4147
|
}] } });
|
|
3608
4148
|
|
|
3609
4149
|
class MnWizardBodyComponent {
|
|
@@ -3638,6 +4178,11 @@ class MnWizardBodyComponent {
|
|
|
3638
4178
|
// Pre-build form configs for all form-driven steps
|
|
3639
4179
|
for (const step of this.config.steps) {
|
|
3640
4180
|
if (step.fields && step.fields.length > 0) {
|
|
4181
|
+
// Merge top-level initialValue with step-level initialValue
|
|
4182
|
+
const mergedInitialValue = {
|
|
4183
|
+
...(this.config.initialValue || {}),
|
|
4184
|
+
...(step.initialValue || {})
|
|
4185
|
+
};
|
|
3641
4186
|
this.stepFormConfigs[step.id] = {
|
|
3642
4187
|
kind: ModalKind.FORM,
|
|
3643
4188
|
fields: step.fields,
|
|
@@ -3645,7 +4190,9 @@ class MnWizardBodyComponent {
|
|
|
3645
4190
|
fieldGroups: step.fieldGroups,
|
|
3646
4191
|
formValidators: step.formValidators,
|
|
3647
4192
|
groupValidators: step.groupValidators,
|
|
3648
|
-
initialValue:
|
|
4193
|
+
initialValue: mergedInitialValue,
|
|
4194
|
+
readOnly: this.config.readOnly,
|
|
4195
|
+
disabled: this.config.disabled
|
|
3649
4196
|
};
|
|
3650
4197
|
}
|
|
3651
4198
|
}
|
|
@@ -3658,6 +4205,10 @@ class MnWizardBodyComponent {
|
|
|
3658
4205
|
// Subscribe to form bodies list changes to track validity
|
|
3659
4206
|
this.formBodiesSubscription = this.formBodies.changes.subscribe(() => {
|
|
3660
4207
|
this.trackCurrentStepValidity();
|
|
4208
|
+
// Apply autofocus when form bodies change (e.g. step navigation)
|
|
4209
|
+
setTimeout(() => {
|
|
4210
|
+
this.getCurrentFormBody()?.applyAutoFocus();
|
|
4211
|
+
}, 100);
|
|
3661
4212
|
});
|
|
3662
4213
|
// Initial validity check
|
|
3663
4214
|
this.trackCurrentStepValidity();
|
|
@@ -3883,11 +4434,11 @@ class MnWizardBodyComponent {
|
|
|
3883
4434
|
}
|
|
3884
4435
|
}
|
|
3885
4436
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
3886
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\">\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ canGoBack ? labels.back : labels.close }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ isCompleting ? labels.completing : labels.complete }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
4437
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\">\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"block\"\n ></mn-custom-body-host>\n\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n *ngIf=\"!currentStep?.hideBack\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
3887
4438
|
}
|
|
3888
4439
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
|
|
3889
4440
|
type: Component,
|
|
3890
|
-
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent], template: "<div class=\"flex flex-col gap-6\">\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ canGoBack ? labels.back : labels.close }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ isCompleting ? labels.completing : labels.complete }}\n </button>\n </div>\n</div>\n" }]
|
|
4441
|
+
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent], template: "<div class=\"flex flex-col gap-6\">\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"block\"\n ></mn-custom-body-host>\n\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n *ngIf=\"!currentStep?.hideBack\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n </div>\n</div>\n" }]
|
|
3891
4442
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
|
|
3892
4443
|
type: Input
|
|
3893
4444
|
}], modalRef: [{
|
|
@@ -3898,10 +4449,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3898
4449
|
}] } });
|
|
3899
4450
|
|
|
3900
4451
|
class MnConfirmationBodyComponent {
|
|
4452
|
+
cdr;
|
|
3901
4453
|
config;
|
|
3902
4454
|
modalRef;
|
|
4455
|
+
formBody;
|
|
4456
|
+
confirmButtonStatus = 'VALID';
|
|
4457
|
+
hasFormFields = false;
|
|
4458
|
+
constructor(cdr) {
|
|
4459
|
+
this.cdr = cdr;
|
|
4460
|
+
}
|
|
4461
|
+
ngOnInit() {
|
|
4462
|
+
this.hasFormFields = !!((this.config.fields && this.config.fields.length > 0) ||
|
|
4463
|
+
(this.config.fieldGroups && this.config.fieldGroups.length > 0) ||
|
|
4464
|
+
(this.config.rows && this.config.rows.length > 0));
|
|
4465
|
+
}
|
|
4466
|
+
onFormStatusChange(status) {
|
|
4467
|
+
this.confirmButtonStatus = status;
|
|
4468
|
+
this.cdr.markForCheck();
|
|
4469
|
+
this.cdr.detectChanges();
|
|
4470
|
+
}
|
|
3903
4471
|
async confirm() {
|
|
3904
|
-
|
|
4472
|
+
if (this.hasFormFields && this.formBody?.form.invalid) {
|
|
4473
|
+
this.formBody.form.markAllAsTouched();
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
const result = (this.hasFormFields ? this.formBody?.form.value : true);
|
|
3905
4477
|
if (this.config.confirm?.handler) {
|
|
3906
4478
|
await this.config.confirm.handler.handle(result);
|
|
3907
4479
|
}
|
|
@@ -3956,75 +4528,31 @@ class MnConfirmationBodyComponent {
|
|
|
3956
4528
|
return 'outline';
|
|
3957
4529
|
}
|
|
3958
4530
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
}
|
|
3962
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
|
|
3963
|
-
type: Component,
|
|
3964
|
-
args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton], template: "<div class=\"flex flex-col items-center gap-6 text-center\" [ngClass]=\"toneClass\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle)\n }\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
|
|
3965
|
-
}], propDecorators: { config: [{
|
|
3966
|
-
type: Input
|
|
3967
|
-
}], modalRef: [{
|
|
3968
|
-
type: Input
|
|
3969
|
-
}] } });
|
|
3970
|
-
|
|
3971
|
-
class MnCustomBodyHostComponent {
|
|
3972
|
-
config;
|
|
3973
|
-
modalRef;
|
|
3974
|
-
container;
|
|
3975
|
-
componentRef;
|
|
3976
|
-
ngOnInit() {
|
|
3977
|
-
setTimeout(() => this.loadContent(), 0);
|
|
3978
|
-
}
|
|
3979
|
-
loadContent() {
|
|
3980
|
-
if (!this.container)
|
|
3981
|
-
return;
|
|
3982
|
-
this.container.clear();
|
|
3983
|
-
if (this.config.component) {
|
|
3984
|
-
this.attachComponent(this.config.component);
|
|
3985
|
-
}
|
|
3986
|
-
else if (this.config.template) {
|
|
3987
|
-
this.attachTemplate(this.config.template);
|
|
3988
|
-
}
|
|
4531
|
+
asAny(val) {
|
|
4532
|
+
return val;
|
|
3989
4533
|
}
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
if (this.config.inputs) {
|
|
3994
|
-
Object.entries(this.config.inputs).forEach(([key, value]) => {
|
|
3995
|
-
this.componentRef.instance[key] = value;
|
|
3996
|
-
});
|
|
3997
|
-
}
|
|
3998
|
-
// Pass modalRef if the component has a modalRef property
|
|
3999
|
-
const instance = this.componentRef.instance;
|
|
4000
|
-
if (instance && 'modalRef' in instance) {
|
|
4001
|
-
instance.modalRef = this.modalRef;
|
|
4534
|
+
get isConfirmDisabled() {
|
|
4535
|
+
if (this.hasFormFields) {
|
|
4536
|
+
return this.confirmButtonStatus !== 'VALID';
|
|
4002
4537
|
}
|
|
4538
|
+
return false;
|
|
4003
4539
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
$implicit: this.modalRef,
|
|
4007
|
-
modalRef: this.modalRef,
|
|
4008
|
-
});
|
|
4540
|
+
asField(field) {
|
|
4541
|
+
return field;
|
|
4009
4542
|
}
|
|
4010
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type:
|
|
4011
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type:
|
|
4543
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
4544
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnConfirmationBodyComponent, isStandalone: true, selector: "mn-confirmation-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBody", first: true, predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\" *ngIf=\"config.message\">\n {{ config.message }}\n </div>\n </div>\n\n <!-- Form Fields / Rows -->\n <mn-form-body\n *ngIf=\"hasFormFields\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
4012
4545
|
}
|
|
4013
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type:
|
|
4546
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
|
|
4014
4547
|
type: Component,
|
|
4015
|
-
args: [{
|
|
4016
|
-
|
|
4017
|
-
standalone: true,
|
|
4018
|
-
imports: [CommonModule],
|
|
4019
|
-
template: '<ng-container #container></ng-container>',
|
|
4020
|
-
}]
|
|
4021
|
-
}], propDecorators: { config: [{
|
|
4548
|
+
args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, ReactiveFormsModule], template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\" *ngIf=\"config.message\">\n {{ config.message }}\n </div>\n </div>\n\n <!-- Form Fields / Rows -->\n <mn-form-body\n *ngIf=\"hasFormFields\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
|
|
4549
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
|
|
4022
4550
|
type: Input
|
|
4023
4551
|
}], modalRef: [{
|
|
4024
4552
|
type: Input
|
|
4025
|
-
}],
|
|
4553
|
+
}], formBody: [{
|
|
4026
4554
|
type: ViewChild,
|
|
4027
|
-
args: [
|
|
4555
|
+
args: [MnFormBodyComponent]
|
|
4028
4556
|
}] } });
|
|
4029
4557
|
|
|
4030
4558
|
class MnModalShellComponent {
|
|
@@ -4032,6 +4560,7 @@ class MnModalShellComponent {
|
|
|
4032
4560
|
config;
|
|
4033
4561
|
modalRef;
|
|
4034
4562
|
isClosing = false;
|
|
4563
|
+
isStacked = false;
|
|
4035
4564
|
ModalKind = ModalKind;
|
|
4036
4565
|
previouslyFocusedElement = null;
|
|
4037
4566
|
focusTrapListener = null;
|
|
@@ -4108,7 +4637,12 @@ class MnModalShellComponent {
|
|
|
4108
4637
|
get hostClasses() {
|
|
4109
4638
|
const size = this.config.size || ModalSize.MD;
|
|
4110
4639
|
const closing = this.isClosing ? ' closing' : '';
|
|
4111
|
-
|
|
4640
|
+
const animType = typeof this.config.animation === 'string'
|
|
4641
|
+
? this.config.animation
|
|
4642
|
+
: this.config.animation?.type || 'slide';
|
|
4643
|
+
const animation = ` anim-${animType}`;
|
|
4644
|
+
const stacked = this.isStacked ? ' is-stacked' : '';
|
|
4645
|
+
return `modal-shell modal-${size}${closing}${animation}${stacked}`;
|
|
4112
4646
|
}
|
|
4113
4647
|
startClosing() {
|
|
4114
4648
|
this.isClosing = true;
|
|
@@ -4159,6 +4693,17 @@ class MnModalShellComponent {
|
|
|
4159
4693
|
get showCloseButton() {
|
|
4160
4694
|
return this.config.closeMode !== CloseMode.DISABLED;
|
|
4161
4695
|
}
|
|
4696
|
+
get animationClass() {
|
|
4697
|
+
const animType = typeof this.config.animation === 'string'
|
|
4698
|
+
? this.config.animation
|
|
4699
|
+
: this.config.animation?.type || 'slide';
|
|
4700
|
+
switch (animType) {
|
|
4701
|
+
case 'fade': return 'animate-[fadeIn_0.2s_ease-in-out]';
|
|
4702
|
+
case 'zoom': return 'animate-[zoomIn_0.2s_ease-in-out]';
|
|
4703
|
+
case 'slide':
|
|
4704
|
+
default: return 'animate-[slideIn_0.2s_ease-in-out]';
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4162
4707
|
// =========================
|
|
4163
4708
|
// Footer Actions
|
|
4164
4709
|
// =========================
|
|
@@ -4240,7 +4785,7 @@ class MnModalShellComponent {
|
|
|
4240
4785
|
}
|
|
4241
4786
|
}
|
|
4242
4787
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
4243
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col
|
|
4788
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [ngClass]=\"[containerSizeClass, animationClass]\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n <div class=\"flex items-center justify-between p-6 border-b border-gray-200\">\n <div class=\"flex flex-col gap-0.5\">\n <h2 class=\"m-0 text-xl font-semibold text-gray-900\" *ngIf=\"config.title\" id=\"mn-modal-title\">{{ config.title }}</h2>\n <p class=\"m-0 text-sm text-gray-500 font-normal\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n </div>\n <button\n *ngIf=\"showCloseButton\"\n class=\"bg-transparent border-none text-2xl cursor-pointer text-gray-500 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-gray-100 hover:text-gray-900\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n </div>\n <p class=\"m-0 px-6 text-sm text-gray-500 leading-relaxed\" *ngIf=\"config.description\" id=\"mn-modal-description\">{{ config.description }}</p>\n\n <div class=\"flex-1 overflow-y-auto p-6\">\n <mn-wizard-body\n *ngIf=\"config.kind === ModalKind.WIZARD\"\n [config]=\"asWizard(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-wizard-body>\n\n <mn-form-body\n *ngIf=\"config.kind === ModalKind.FORM\"\n [config]=\"asForm(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-form-body>\n\n <mn-confirmation-body\n *ngIf=\"config.kind === ModalKind.CONFIRMATION\"\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-confirmation-body>\n\n <mn-custom-body-host\n *ngIf=\"config.kind === ModalKind.CUSTOM\"\n [config]=\"asCustom(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n </div>\n\n <!-- Custom Footer Actions -->\n <div *ngIf=\"hasCustomFooterActions\" class=\"flex gap-3 p-6 border-t border-gray-200\">\n <ng-container *ngFor=\"let action of leftFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n\n <div class=\"flex-1\"></div>\n\n <ng-container *ngFor=\"let action of rightFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }] });
|
|
4244
4789
|
}
|
|
4245
4790
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
4246
4791
|
type: Component,
|
|
@@ -4251,7 +4796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4251
4796
|
MnConfirmationBodyComponent,
|
|
4252
4797
|
MnCustomBodyHostComponent,
|
|
4253
4798
|
MnButton,
|
|
4254
|
-
], template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col
|
|
4799
|
+
], template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [ngClass]=\"[containerSizeClass, animationClass]\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n <div class=\"flex items-center justify-between p-6 border-b border-gray-200\">\n <div class=\"flex flex-col gap-0.5\">\n <h2 class=\"m-0 text-xl font-semibold text-gray-900\" *ngIf=\"config.title\" id=\"mn-modal-title\">{{ config.title }}</h2>\n <p class=\"m-0 text-sm text-gray-500 font-normal\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n </div>\n <button\n *ngIf=\"showCloseButton\"\n class=\"bg-transparent border-none text-2xl cursor-pointer text-gray-500 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-gray-100 hover:text-gray-900\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n </div>\n <p class=\"m-0 px-6 text-sm text-gray-500 leading-relaxed\" *ngIf=\"config.description\" id=\"mn-modal-description\">{{ config.description }}</p>\n\n <div class=\"flex-1 overflow-y-auto p-6\">\n <mn-wizard-body\n *ngIf=\"config.kind === ModalKind.WIZARD\"\n [config]=\"asWizard(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-wizard-body>\n\n <mn-form-body\n *ngIf=\"config.kind === ModalKind.FORM\"\n [config]=\"asForm(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-form-body>\n\n <mn-confirmation-body\n *ngIf=\"config.kind === ModalKind.CONFIRMATION\"\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-confirmation-body>\n\n <mn-custom-body-host\n *ngIf=\"config.kind === ModalKind.CUSTOM\"\n [config]=\"asCustom(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n </div>\n\n <!-- Custom Footer Actions -->\n <div *ngIf=\"hasCustomFooterActions\" class=\"flex gap-3 p-6 border-t border-gray-200\">\n <ng-container *ngFor=\"let action of leftFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n\n <div class=\"flex-1\"></div>\n\n <ng-container *ngFor=\"let action of rightFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}\n"] }]
|
|
4255
4800
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { config: [{
|
|
4256
4801
|
type: Input
|
|
4257
4802
|
}], modalRef: [{
|
|
@@ -4267,6 +4812,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4267
4812
|
class MnModalService {
|
|
4268
4813
|
appRef = inject(ApplicationRef);
|
|
4269
4814
|
injector = inject(EnvironmentInjector);
|
|
4815
|
+
modalStack = [];
|
|
4270
4816
|
open(config) {
|
|
4271
4817
|
// Create the modal shell component
|
|
4272
4818
|
const componentRef = createComponent(MnModalShellComponent, {
|
|
@@ -4277,6 +4823,12 @@ class MnModalService {
|
|
|
4277
4823
|
// Create modal ref
|
|
4278
4824
|
const modalRef = new MnModalRef(componentRef, config);
|
|
4279
4825
|
componentRef.instance.modalRef = modalRef;
|
|
4826
|
+
// Update stack and dim previous modal
|
|
4827
|
+
if (this.modalStack.length > 0) {
|
|
4828
|
+
const prevModal = this.modalStack[this.modalStack.length - 1];
|
|
4829
|
+
prevModal.component.isStacked = true;
|
|
4830
|
+
}
|
|
4831
|
+
this.modalStack.push(modalRef);
|
|
4280
4832
|
// Attach to application
|
|
4281
4833
|
this.appRef.attachView(componentRef.hostView);
|
|
4282
4834
|
const domElem = componentRef.location.nativeElement;
|
|
@@ -4285,6 +4837,15 @@ class MnModalService {
|
|
|
4285
4837
|
modalRef.afterClosed$.subscribe(() => {
|
|
4286
4838
|
this.appRef.detachView(componentRef.hostView);
|
|
4287
4839
|
domElem.remove();
|
|
4840
|
+
// Update stack
|
|
4841
|
+
const index = this.modalStack.indexOf(modalRef);
|
|
4842
|
+
if (index > -1) {
|
|
4843
|
+
this.modalStack.splice(index, 1);
|
|
4844
|
+
if (this.modalStack.length > 0) {
|
|
4845
|
+
const topModal = this.modalStack[this.modalStack.length - 1];
|
|
4846
|
+
topModal.component.isStacked = false;
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4288
4849
|
});
|
|
4289
4850
|
return modalRef;
|
|
4290
4851
|
}
|
|
@@ -4789,6 +5350,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4789
5350
|
}]
|
|
4790
5351
|
}], ctorParameters: () => [{ type: MnLanguageService }] });
|
|
4791
5352
|
|
|
5353
|
+
/**
|
|
5354
|
+
* Enable live preview mode. Listens for postMessage events from
|
|
5355
|
+
* Mn Web Manager and hot-swaps config/translations at runtime.
|
|
5356
|
+
*
|
|
5357
|
+
* Call this once in your app's bootstrap (e.g., APP_INITIALIZER or root component).
|
|
5358
|
+
*
|
|
5359
|
+
* @param configService - The MnConfigService instance
|
|
5360
|
+
* @param langService - The MnLanguageService instance
|
|
5361
|
+
* @param allowedOrigins - Optional whitelist of allowed origins (security)
|
|
5362
|
+
*/
|
|
5363
|
+
function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
5364
|
+
window.addEventListener('message', async (event) => {
|
|
5365
|
+
if (allowedOrigins?.length && !allowedOrigins.includes(event.origin)) {
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
5368
|
+
const data = event.data;
|
|
5369
|
+
if (!data?.type)
|
|
5370
|
+
return;
|
|
5371
|
+
switch (data.type) {
|
|
5372
|
+
case 'mn-config-update':
|
|
5373
|
+
if (data.config) {
|
|
5374
|
+
await configService.loadFromObject(data.config);
|
|
5375
|
+
}
|
|
5376
|
+
break;
|
|
5377
|
+
case 'mn-translations-update':
|
|
5378
|
+
if (data.translations) {
|
|
5379
|
+
for (const [locale, translations] of Object.entries(data.translations)) {
|
|
5380
|
+
langService.registerTranslations(locale, translations);
|
|
5381
|
+
}
|
|
5382
|
+
await langService.setLocale(langService.locale);
|
|
5383
|
+
}
|
|
5384
|
+
break;
|
|
5385
|
+
}
|
|
5386
|
+
});
|
|
5387
|
+
}
|
|
5388
|
+
|
|
4792
5389
|
/*
|
|
4793
5390
|
* Public API Surface of mn-lib
|
|
4794
5391
|
*/
|
|
@@ -4797,5 +5394,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4797
5394
|
* Generated bundle index. Do not edit.
|
|
4798
5395
|
*/
|
|
4799
5396
|
|
|
4800
|
-
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_MN_ALERT_CONFIG, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_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_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig, provideMnLanguage };
|
|
5397
|
+
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_MN_ALERT_CONFIG, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_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_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig, provideMnLanguage };
|
|
4801
5398
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|