mn-angular-lib 0.0.51 → 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}`;
|
|
@@ -1581,11 +1698,11 @@ class MnTextarea {
|
|
|
1581
1698
|
});
|
|
1582
1699
|
}
|
|
1583
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 });
|
|
1584
|
-
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"] }] });
|
|
1585
1702
|
}
|
|
1586
1703
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTextarea, decorators: [{
|
|
1587
1704
|
type: Component,
|
|
1588
|
-
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" }]
|
|
1589
1706
|
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
1590
1707
|
type: Optional
|
|
1591
1708
|
}, {
|
|
@@ -2307,53 +2424,41 @@ var ValidationCode;
|
|
|
2307
2424
|
ValidationCode["CUSTOM"] = "custom";
|
|
2308
2425
|
})(ValidationCode || (ValidationCode = {}));
|
|
2309
2426
|
|
|
2310
|
-
|
|
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 {
|
|
2311
2432
|
config;
|
|
2433
|
+
parent;
|
|
2312
2434
|
currentRow = [];
|
|
2313
2435
|
currentRowColumns = 1;
|
|
2314
|
-
constructor(
|
|
2315
|
-
this.config =
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
state: StepState.PENDING,
|
|
2320
|
-
};
|
|
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 || [];
|
|
2321
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Add a custom body/content to the form/step.
|
|
2444
|
+
*/
|
|
2322
2445
|
body(body) {
|
|
2323
2446
|
this.config.body = body;
|
|
2324
|
-
return this;
|
|
2325
|
-
}
|
|
2326
|
-
state(state) {
|
|
2327
|
-
this.config.state = state;
|
|
2328
|
-
return this;
|
|
2329
|
-
}
|
|
2330
|
-
guard(guard) {
|
|
2331
|
-
this.config.guard = guard;
|
|
2332
|
-
return this;
|
|
2333
|
-
}
|
|
2334
|
-
validators(validators) {
|
|
2335
|
-
this.config.validators = validators;
|
|
2336
|
-
return this;
|
|
2447
|
+
return this.parent;
|
|
2337
2448
|
}
|
|
2338
2449
|
/**
|
|
2339
|
-
* Add a field
|
|
2340
|
-
*
|
|
2341
|
-
* @example
|
|
2342
|
-
* s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
|
|
2343
|
-
* s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
|
|
2450
|
+
* Add a field as a full-width row (single column).
|
|
2344
2451
|
*/
|
|
2345
2452
|
field(field) {
|
|
2346
2453
|
this.flushCurrentRow();
|
|
2347
2454
|
this.config.fields = this.config.fields || [];
|
|
2348
2455
|
this.config.fields.push(field);
|
|
2349
|
-
|
|
2350
|
-
this.config.rows = [];
|
|
2351
|
-
}
|
|
2456
|
+
this.config.rows = this.config.rows || [];
|
|
2352
2457
|
this.config.rows.push({
|
|
2353
2458
|
columns: 1,
|
|
2354
2459
|
fields: [{ field, span: 1 }],
|
|
2355
2460
|
});
|
|
2356
|
-
return this;
|
|
2461
|
+
return this.parent;
|
|
2357
2462
|
}
|
|
2358
2463
|
/**
|
|
2359
2464
|
* Start a new row with the specified number of columns.
|
|
@@ -2362,70 +2467,293 @@ class StepBuilder {
|
|
|
2362
2467
|
row(columns = 2) {
|
|
2363
2468
|
this.flushCurrentRow();
|
|
2364
2469
|
this.currentRowColumns = columns;
|
|
2365
|
-
return this;
|
|
2470
|
+
return this.parent;
|
|
2366
2471
|
}
|
|
2367
2472
|
/**
|
|
2368
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)
|
|
2369
2476
|
*/
|
|
2370
2477
|
addToRow(field, span = 1) {
|
|
2371
2478
|
this.config.fields = this.config.fields || [];
|
|
2372
2479
|
this.config.fields.push(field);
|
|
2373
2480
|
this.currentRow.push({ field, span });
|
|
2374
|
-
return this;
|
|
2481
|
+
return this.parent;
|
|
2375
2482
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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 || [];
|
|
2381
2503
|
this.config.rows.push({
|
|
2382
|
-
columns
|
|
2383
|
-
fields:
|
|
2504
|
+
columns,
|
|
2505
|
+
fields: rowFields,
|
|
2384
2506
|
});
|
|
2385
|
-
this.currentRow = [];
|
|
2386
|
-
this.currentRowColumns = 1;
|
|
2387
2507
|
}
|
|
2508
|
+
return this.parent;
|
|
2388
2509
|
}
|
|
2389
|
-
|
|
2390
|
-
* Add a field group with a section header.
|
|
2391
|
-
*/
|
|
2392
|
-
fieldGroup(group) {
|
|
2510
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2393
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) {
|
|
2394
2546
|
if (!this.config.fieldGroups) {
|
|
2395
2547
|
this.config.fieldGroups = [];
|
|
2396
2548
|
}
|
|
2549
|
+
// Also add group fields to the flat fields array for form control creation
|
|
2397
2550
|
this.config.fields = this.config.fields || [];
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
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) => ({
|
|
2401
2556
|
columns: 1,
|
|
2402
2557
|
fields: [{ field: f, span: 1 }],
|
|
2403
2558
|
}));
|
|
2404
2559
|
}
|
|
2405
2560
|
this.config.fieldGroups.push(group);
|
|
2406
|
-
return this;
|
|
2407
2561
|
}
|
|
2408
2562
|
/**
|
|
2409
|
-
* Add form-level validators for cross-field validation
|
|
2563
|
+
* Add form-level validators for cross-field validation.
|
|
2410
2564
|
*/
|
|
2411
2565
|
formValidators(validators) {
|
|
2412
2566
|
this.config.formValidators = validators;
|
|
2413
|
-
return this;
|
|
2567
|
+
return this.parent;
|
|
2414
2568
|
}
|
|
2415
2569
|
/**
|
|
2416
|
-
* Add Angular FormGroup-level validators
|
|
2570
|
+
* Add Angular FormGroup-level validators.
|
|
2417
2571
|
*/
|
|
2418
2572
|
groupValidators(validators) {
|
|
2419
2573
|
this.config.groupValidators = validators;
|
|
2420
|
-
return this;
|
|
2574
|
+
return this.parent;
|
|
2421
2575
|
}
|
|
2422
2576
|
/**
|
|
2423
|
-
* Set initial
|
|
2577
|
+
* Set initial value for fields.
|
|
2424
2578
|
*/
|
|
2425
2579
|
initialValue(value) {
|
|
2426
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;
|
|
2427
2701
|
return this;
|
|
2428
2702
|
}
|
|
2703
|
+
validators(validators) {
|
|
2704
|
+
this.config.validators = validators;
|
|
2705
|
+
return this;
|
|
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
|
+
}
|
|
2429
2757
|
/**
|
|
2430
2758
|
* Set a visibility condition for this step based on aggregated wizard data.
|
|
2431
2759
|
*/
|
|
@@ -2433,16 +2761,40 @@ class StepBuilder {
|
|
|
2433
2761
|
this.config.visible = condition;
|
|
2434
2762
|
return this;
|
|
2435
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
|
+
}
|
|
2436
2785
|
build() {
|
|
2437
|
-
this.flushCurrentRow();
|
|
2786
|
+
this.layoutBuilder.flushCurrentRow();
|
|
2438
2787
|
return this.config;
|
|
2439
2788
|
}
|
|
2440
2789
|
}
|
|
2441
2790
|
|
|
2442
2791
|
class BaseModalBuilder {
|
|
2443
2792
|
config;
|
|
2793
|
+
layoutBuilder;
|
|
2444
2794
|
constructor(initialConfig) {
|
|
2445
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);
|
|
2446
2798
|
}
|
|
2447
2799
|
title(title) {
|
|
2448
2800
|
this.config.title = title;
|
|
@@ -2480,6 +2832,14 @@ class BaseModalBuilder {
|
|
|
2480
2832
|
this.config.intent = intent;
|
|
2481
2833
|
return this;
|
|
2482
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
|
+
}
|
|
2483
2843
|
footerActions(actions) {
|
|
2484
2844
|
this.config.footerActions = actions;
|
|
2485
2845
|
return this;
|
|
@@ -2496,7 +2856,63 @@ class BaseModalBuilder {
|
|
|
2496
2856
|
this.config.i18n = labels;
|
|
2497
2857
|
return this;
|
|
2498
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
|
+
}
|
|
2499
2914
|
build() {
|
|
2915
|
+
this.layoutBuilder.flushCurrentRow();
|
|
2500
2916
|
return Object.freeze({ ...this.config });
|
|
2501
2917
|
}
|
|
2502
2918
|
}
|
|
@@ -2508,6 +2924,32 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2508
2924
|
steps: [],
|
|
2509
2925
|
});
|
|
2510
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
|
+
}
|
|
2511
2953
|
step(step) {
|
|
2512
2954
|
if (typeof step === 'function') {
|
|
2513
2955
|
const builder = new StepBuilder('', '');
|
|
@@ -2524,7 +2966,8 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2524
2966
|
const stepId = id || `step-${this.config.steps.length}`;
|
|
2525
2967
|
const builder = new StepBuilder(stepId, title);
|
|
2526
2968
|
buildFn(builder);
|
|
2527
|
-
|
|
2969
|
+
const builtStep = builder.build();
|
|
2970
|
+
this.config.steps.push(builtStep);
|
|
2528
2971
|
return this;
|
|
2529
2972
|
}
|
|
2530
2973
|
startAt(stepId) {
|
|
@@ -2550,8 +2993,6 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2550
2993
|
}
|
|
2551
2994
|
|
|
2552
2995
|
class FormModalBuilder extends BaseModalBuilder {
|
|
2553
|
-
currentRow = [];
|
|
2554
|
-
currentRowColumns = 1;
|
|
2555
2996
|
constructor() {
|
|
2556
2997
|
super({
|
|
2557
2998
|
kind: ModalKind.FORM,
|
|
@@ -2559,64 +3000,27 @@ class FormModalBuilder extends BaseModalBuilder {
|
|
|
2559
3000
|
rows: [],
|
|
2560
3001
|
});
|
|
2561
3002
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
this.config.fields.push(field);
|
|
2569
|
-
this.config.rows.push({
|
|
2570
|
-
columns: 1,
|
|
2571
|
-
fields: [{ field, span: 1 }],
|
|
2572
|
-
});
|
|
2573
|
-
return this;
|
|
2574
|
-
}
|
|
2575
|
-
/**
|
|
2576
|
-
* Start a new row with the specified number of columns.
|
|
2577
|
-
* All subsequent `addToRow()` calls will add fields to this row
|
|
2578
|
-
* until the next `row()` or `field()` call.
|
|
2579
|
-
*
|
|
2580
|
-
* @example
|
|
2581
|
-
* .row(2)
|
|
2582
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'firstName', label: 'First Name' })
|
|
2583
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'lastName', label: 'Last Name' })
|
|
2584
|
-
* .row(3)
|
|
2585
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'city', label: 'City' }, 2)
|
|
2586
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'zip', label: 'ZIP' })
|
|
2587
|
-
*/
|
|
3003
|
+
body(body) {
|
|
3004
|
+
return super.body(body);
|
|
3005
|
+
}
|
|
3006
|
+
field(field) {
|
|
3007
|
+
return super.field(field);
|
|
3008
|
+
}
|
|
2588
3009
|
row(columns = 2) {
|
|
2589
|
-
|
|
2590
|
-
this.currentRowColumns = columns;
|
|
2591
|
-
return this;
|
|
3010
|
+
return super.row(columns);
|
|
2592
3011
|
}
|
|
2593
|
-
/**
|
|
2594
|
-
* Add a field to the current row started by `row()`.
|
|
2595
|
-
* @param field - The field configuration
|
|
2596
|
-
* @param span - How many columns this field should span (default: 1)
|
|
2597
|
-
*/
|
|
2598
3012
|
addToRow(field, span = 1) {
|
|
2599
|
-
|
|
2600
|
-
this.currentRow.push({ field, span });
|
|
2601
|
-
return this;
|
|
3013
|
+
return super.addToRow(field, span);
|
|
2602
3014
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
this.config.rows.push({
|
|
2606
|
-
columns: this.currentRowColumns,
|
|
2607
|
-
fields: [...this.currentRow],
|
|
2608
|
-
});
|
|
2609
|
-
this.currentRow = [];
|
|
2610
|
-
this.currentRowColumns = 1;
|
|
2611
|
-
}
|
|
3015
|
+
addRow(columns, buildFn) {
|
|
3016
|
+
return super.addRow(columns, buildFn);
|
|
2612
3017
|
}
|
|
2613
3018
|
layout(mode) {
|
|
2614
3019
|
this.config.layout = mode;
|
|
2615
3020
|
return this;
|
|
2616
3021
|
}
|
|
2617
3022
|
initialValue(value) {
|
|
2618
|
-
this.
|
|
2619
|
-
return this;
|
|
3023
|
+
return this.layoutBuilder.initialValue(value);
|
|
2620
3024
|
}
|
|
2621
3025
|
submitMode(mode) {
|
|
2622
3026
|
this.config.submitMode = mode;
|
|
@@ -2626,45 +3030,16 @@ class FormModalBuilder extends BaseModalBuilder {
|
|
|
2626
3030
|
this.config.onComplete = handler;
|
|
2627
3031
|
return this;
|
|
2628
3032
|
}
|
|
2629
|
-
/**
|
|
2630
|
-
* Add form-level validators for cross-field validation.
|
|
2631
|
-
* These receive the entire form value and return an error map or null.
|
|
2632
|
-
*/
|
|
2633
3033
|
formValidators(validators) {
|
|
2634
|
-
this.
|
|
2635
|
-
return this;
|
|
3034
|
+
return this.layoutBuilder.formValidators(validators);
|
|
2636
3035
|
}
|
|
2637
|
-
/**
|
|
2638
|
-
* Add Angular FormGroup-level validators.
|
|
2639
|
-
* These are standard Angular ValidatorFn applied to the FormGroup itself.
|
|
2640
|
-
*/
|
|
2641
3036
|
groupValidators(validators) {
|
|
2642
|
-
this.
|
|
2643
|
-
return this;
|
|
3037
|
+
return this.layoutBuilder.groupValidators(validators);
|
|
2644
3038
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
* Groups visually separate fields with a title and optional description.
|
|
2648
|
-
*/
|
|
2649
|
-
fieldGroup(group) {
|
|
2650
|
-
this.flushCurrentRow();
|
|
2651
|
-
if (!this.config.fieldGroups) {
|
|
2652
|
-
this.config.fieldGroups = [];
|
|
2653
|
-
}
|
|
2654
|
-
// Also add group fields to the flat fields array for form control creation
|
|
2655
|
-
group.fields.forEach(f => this.config.fields.push(f));
|
|
2656
|
-
// Build rows for the group if not provided
|
|
2657
|
-
if (!group.rows) {
|
|
2658
|
-
group.rows = group.fields.map(f => ({
|
|
2659
|
-
columns: 1,
|
|
2660
|
-
fields: [{ field: f, span: 1 }],
|
|
2661
|
-
}));
|
|
2662
|
-
}
|
|
2663
|
-
this.config.fieldGroups.push(group);
|
|
2664
|
-
return this;
|
|
3039
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
3040
|
+
return super.fieldGroup(arg1, arg2, arg3);
|
|
2665
3041
|
}
|
|
2666
3042
|
build() {
|
|
2667
|
-
this.flushCurrentRow();
|
|
2668
3043
|
return super.build();
|
|
2669
3044
|
}
|
|
2670
3045
|
}
|
|
@@ -2674,6 +3049,8 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
|
|
|
2674
3049
|
super({
|
|
2675
3050
|
kind: ModalKind.CONFIRMATION,
|
|
2676
3051
|
message: '',
|
|
3052
|
+
fields: [],
|
|
3053
|
+
rows: [],
|
|
2677
3054
|
});
|
|
2678
3055
|
}
|
|
2679
3056
|
message(text) {
|
|
@@ -2692,6 +3069,36 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
|
|
|
2692
3069
|
this.config.cancel = action;
|
|
2693
3070
|
return this;
|
|
2694
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
|
+
}
|
|
2695
3102
|
}
|
|
2696
3103
|
|
|
2697
3104
|
class CustomModalBuilder extends BaseModalBuilder {
|
|
@@ -2700,18 +3107,6 @@ class CustomModalBuilder extends BaseModalBuilder {
|
|
|
2700
3107
|
kind: ModalKind.CUSTOM,
|
|
2701
3108
|
});
|
|
2702
3109
|
}
|
|
2703
|
-
component(component) {
|
|
2704
|
-
this.config.component = component;
|
|
2705
|
-
return this;
|
|
2706
|
-
}
|
|
2707
|
-
template(template) {
|
|
2708
|
-
this.config.template = template;
|
|
2709
|
-
return this;
|
|
2710
|
-
}
|
|
2711
|
-
inputs(inputs) {
|
|
2712
|
-
this.config.inputs = inputs;
|
|
2713
|
-
return this;
|
|
2714
|
-
}
|
|
2715
3110
|
onComplete(handler) {
|
|
2716
3111
|
this.config.onComplete = handler;
|
|
2717
3112
|
return this;
|
|
@@ -2769,6 +3164,9 @@ class MnModalRef {
|
|
|
2769
3164
|
// Trigger change detection on the shell component
|
|
2770
3165
|
this.componentRef.changeDetectorRef.detectChanges();
|
|
2771
3166
|
}
|
|
3167
|
+
get component() {
|
|
3168
|
+
return this.componentRef.instance;
|
|
3169
|
+
}
|
|
2772
3170
|
destroy() {
|
|
2773
3171
|
this.componentRef.destroy();
|
|
2774
3172
|
}
|
|
@@ -3091,11 +3489,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3091
3489
|
type: Output
|
|
3092
3490
|
}] } });
|
|
3093
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
|
+
|
|
3094
3551
|
class MnFormBodyComponent {
|
|
3095
3552
|
fb;
|
|
3096
3553
|
config;
|
|
3097
3554
|
modalRef;
|
|
3098
3555
|
hideFooter = false;
|
|
3556
|
+
hideCustomBody = false;
|
|
3557
|
+
formStatusChange = new EventEmitter();
|
|
3558
|
+
inputFields;
|
|
3559
|
+
textareas;
|
|
3099
3560
|
form;
|
|
3100
3561
|
rows = [];
|
|
3101
3562
|
fieldGroups = [];
|
|
@@ -3154,6 +3615,44 @@ class MnFormBodyComponent {
|
|
|
3154
3615
|
this.initializeDataSources();
|
|
3155
3616
|
this.initializeTableFields();
|
|
3156
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);
|
|
3157
3656
|
}
|
|
3158
3657
|
ngOnDestroy() {
|
|
3159
3658
|
this.valueChangesSubscription?.unsubscribe();
|
|
@@ -3169,7 +3668,15 @@ class MnFormBodyComponent {
|
|
|
3169
3668
|
}
|
|
3170
3669
|
const validators = fieldConfig.validators || [];
|
|
3171
3670
|
const asyncValidators = fieldConfig.asyncValidators || [];
|
|
3172
|
-
|
|
3671
|
+
const updateOn = fieldConfig.updateOn || 'change';
|
|
3672
|
+
formControls[field.key] = [
|
|
3673
|
+
initialValue,
|
|
3674
|
+
{
|
|
3675
|
+
validators,
|
|
3676
|
+
asyncValidators,
|
|
3677
|
+
updateOn
|
|
3678
|
+
}
|
|
3679
|
+
];
|
|
3173
3680
|
});
|
|
3174
3681
|
this.form = this.fb.group(formControls);
|
|
3175
3682
|
// Apply Angular FormGroup-level validators
|
|
@@ -3187,10 +3694,10 @@ class MnFormBodyComponent {
|
|
|
3187
3694
|
});
|
|
3188
3695
|
}
|
|
3189
3696
|
isFieldReadOnly(field) {
|
|
3190
|
-
return field.readOnly === true;
|
|
3697
|
+
return this.config.readOnly === true || field.readOnly === true;
|
|
3191
3698
|
}
|
|
3192
3699
|
isFieldDisabled(field) {
|
|
3193
|
-
return field.disabled === true;
|
|
3700
|
+
return this.config.disabled === true || field.disabled === true;
|
|
3194
3701
|
}
|
|
3195
3702
|
/** Track which field groups are currently visible */
|
|
3196
3703
|
groupVisibility = {};
|
|
@@ -3198,12 +3705,25 @@ class MnFormBodyComponent {
|
|
|
3198
3705
|
if (this.config.rows && this.config.rows.length > 0) {
|
|
3199
3706
|
this.rows = this.config.rows;
|
|
3200
3707
|
}
|
|
3201
|
-
else
|
|
3202
|
-
//
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
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
|
+
}
|
|
3207
3727
|
}
|
|
3208
3728
|
// Build field groups
|
|
3209
3729
|
this.fieldGroups = this.config.fieldGroups || [];
|
|
@@ -3482,6 +4002,8 @@ class MnFormBodyComponent {
|
|
|
3482
4002
|
this.runFormValidators();
|
|
3483
4003
|
// Reload data sources that depend on changed fields
|
|
3484
4004
|
this.reloadDependentDataSources(formValue);
|
|
4005
|
+
// Emit status change
|
|
4006
|
+
this.formStatusChange.emit(this.form.status);
|
|
3485
4007
|
});
|
|
3486
4008
|
}
|
|
3487
4009
|
previousFormValue = {};
|
|
@@ -3601,17 +4123,27 @@ class MnFormBodyComponent {
|
|
|
3601
4123
|
}
|
|
3602
4124
|
}
|
|
3603
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 });
|
|
3604
|
-
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"] }] });
|
|
3605
4127
|
}
|
|
3606
4128
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
|
|
3607
4129
|
type: Component,
|
|
3608
|
-
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"] }]
|
|
3609
4131
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
|
|
3610
4132
|
type: Input
|
|
3611
4133
|
}], modalRef: [{
|
|
3612
4134
|
type: Input
|
|
3613
4135
|
}], hideFooter: [{
|
|
3614
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]
|
|
3615
4147
|
}] } });
|
|
3616
4148
|
|
|
3617
4149
|
class MnWizardBodyComponent {
|
|
@@ -3646,6 +4178,11 @@ class MnWizardBodyComponent {
|
|
|
3646
4178
|
// Pre-build form configs for all form-driven steps
|
|
3647
4179
|
for (const step of this.config.steps) {
|
|
3648
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
|
+
};
|
|
3649
4186
|
this.stepFormConfigs[step.id] = {
|
|
3650
4187
|
kind: ModalKind.FORM,
|
|
3651
4188
|
fields: step.fields,
|
|
@@ -3653,7 +4190,9 @@ class MnWizardBodyComponent {
|
|
|
3653
4190
|
fieldGroups: step.fieldGroups,
|
|
3654
4191
|
formValidators: step.formValidators,
|
|
3655
4192
|
groupValidators: step.groupValidators,
|
|
3656
|
-
initialValue:
|
|
4193
|
+
initialValue: mergedInitialValue,
|
|
4194
|
+
readOnly: this.config.readOnly,
|
|
4195
|
+
disabled: this.config.disabled
|
|
3657
4196
|
};
|
|
3658
4197
|
}
|
|
3659
4198
|
}
|
|
@@ -3666,6 +4205,10 @@ class MnWizardBodyComponent {
|
|
|
3666
4205
|
// Subscribe to form bodies list changes to track validity
|
|
3667
4206
|
this.formBodiesSubscription = this.formBodies.changes.subscribe(() => {
|
|
3668
4207
|
this.trackCurrentStepValidity();
|
|
4208
|
+
// Apply autofocus when form bodies change (e.g. step navigation)
|
|
4209
|
+
setTimeout(() => {
|
|
4210
|
+
this.getCurrentFormBody()?.applyAutoFocus();
|
|
4211
|
+
}, 100);
|
|
3669
4212
|
});
|
|
3670
4213
|
// Initial validity check
|
|
3671
4214
|
this.trackCurrentStepValidity();
|
|
@@ -3891,11 +4434,11 @@ class MnWizardBodyComponent {
|
|
|
3891
4434
|
}
|
|
3892
4435
|
}
|
|
3893
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 });
|
|
3894
|
-
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" }] });
|
|
3895
4438
|
}
|
|
3896
4439
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
|
|
3897
4440
|
type: Component,
|
|
3898
|
-
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" }]
|
|
3899
4442
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
|
|
3900
4443
|
type: Input
|
|
3901
4444
|
}], modalRef: [{
|
|
@@ -3906,10 +4449,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3906
4449
|
}] } });
|
|
3907
4450
|
|
|
3908
4451
|
class MnConfirmationBodyComponent {
|
|
4452
|
+
cdr;
|
|
3909
4453
|
config;
|
|
3910
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
|
+
}
|
|
3911
4471
|
async confirm() {
|
|
3912
|
-
|
|
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);
|
|
3913
4477
|
if (this.config.confirm?.handler) {
|
|
3914
4478
|
await this.config.confirm.handler.handle(result);
|
|
3915
4479
|
}
|
|
@@ -3964,75 +4528,31 @@ class MnConfirmationBodyComponent {
|
|
|
3964
4528
|
return 'outline';
|
|
3965
4529
|
}
|
|
3966
4530
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
}
|
|
3970
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
|
|
3971
|
-
type: Component,
|
|
3972
|
-
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" }]
|
|
3973
|
-
}], propDecorators: { config: [{
|
|
3974
|
-
type: Input
|
|
3975
|
-
}], modalRef: [{
|
|
3976
|
-
type: Input
|
|
3977
|
-
}] } });
|
|
3978
|
-
|
|
3979
|
-
class MnCustomBodyHostComponent {
|
|
3980
|
-
config;
|
|
3981
|
-
modalRef;
|
|
3982
|
-
container;
|
|
3983
|
-
componentRef;
|
|
3984
|
-
ngOnInit() {
|
|
3985
|
-
setTimeout(() => this.loadContent(), 0);
|
|
3986
|
-
}
|
|
3987
|
-
loadContent() {
|
|
3988
|
-
if (!this.container)
|
|
3989
|
-
return;
|
|
3990
|
-
this.container.clear();
|
|
3991
|
-
if (this.config.component) {
|
|
3992
|
-
this.attachComponent(this.config.component);
|
|
3993
|
-
}
|
|
3994
|
-
else if (this.config.template) {
|
|
3995
|
-
this.attachTemplate(this.config.template);
|
|
3996
|
-
}
|
|
4531
|
+
asAny(val) {
|
|
4532
|
+
return val;
|
|
3997
4533
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
if (this.config.inputs) {
|
|
4002
|
-
Object.entries(this.config.inputs).forEach(([key, value]) => {
|
|
4003
|
-
this.componentRef.instance[key] = value;
|
|
4004
|
-
});
|
|
4005
|
-
}
|
|
4006
|
-
// Pass modalRef if the component has a modalRef property
|
|
4007
|
-
const instance = this.componentRef.instance;
|
|
4008
|
-
if (instance && 'modalRef' in instance) {
|
|
4009
|
-
instance.modalRef = this.modalRef;
|
|
4534
|
+
get isConfirmDisabled() {
|
|
4535
|
+
if (this.hasFormFields) {
|
|
4536
|
+
return this.confirmButtonStatus !== 'VALID';
|
|
4010
4537
|
}
|
|
4538
|
+
return false;
|
|
4011
4539
|
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
$implicit: this.modalRef,
|
|
4015
|
-
modalRef: this.modalRef,
|
|
4016
|
-
});
|
|
4540
|
+
asField(field) {
|
|
4541
|
+
return field;
|
|
4017
4542
|
}
|
|
4018
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type:
|
|
4019
|
-
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 }] });
|
|
4020
4545
|
}
|
|
4021
|
-
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: [{
|
|
4022
4547
|
type: Component,
|
|
4023
|
-
args: [{
|
|
4024
|
-
|
|
4025
|
-
standalone: true,
|
|
4026
|
-
imports: [CommonModule],
|
|
4027
|
-
template: '<ng-container #container></ng-container>',
|
|
4028
|
-
}]
|
|
4029
|
-
}], 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: [{
|
|
4030
4550
|
type: Input
|
|
4031
4551
|
}], modalRef: [{
|
|
4032
4552
|
type: Input
|
|
4033
|
-
}],
|
|
4553
|
+
}], formBody: [{
|
|
4034
4554
|
type: ViewChild,
|
|
4035
|
-
args: [
|
|
4555
|
+
args: [MnFormBodyComponent]
|
|
4036
4556
|
}] } });
|
|
4037
4557
|
|
|
4038
4558
|
class MnModalShellComponent {
|
|
@@ -4040,6 +4560,7 @@ class MnModalShellComponent {
|
|
|
4040
4560
|
config;
|
|
4041
4561
|
modalRef;
|
|
4042
4562
|
isClosing = false;
|
|
4563
|
+
isStacked = false;
|
|
4043
4564
|
ModalKind = ModalKind;
|
|
4044
4565
|
previouslyFocusedElement = null;
|
|
4045
4566
|
focusTrapListener = null;
|
|
@@ -4116,7 +4637,12 @@ class MnModalShellComponent {
|
|
|
4116
4637
|
get hostClasses() {
|
|
4117
4638
|
const size = this.config.size || ModalSize.MD;
|
|
4118
4639
|
const closing = this.isClosing ? ' closing' : '';
|
|
4119
|
-
|
|
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}`;
|
|
4120
4646
|
}
|
|
4121
4647
|
startClosing() {
|
|
4122
4648
|
this.isClosing = true;
|
|
@@ -4167,6 +4693,17 @@ class MnModalShellComponent {
|
|
|
4167
4693
|
get showCloseButton() {
|
|
4168
4694
|
return this.config.closeMode !== CloseMode.DISABLED;
|
|
4169
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
|
+
}
|
|
4170
4707
|
// =========================
|
|
4171
4708
|
// Footer Actions
|
|
4172
4709
|
// =========================
|
|
@@ -4248,7 +4785,7 @@ class MnModalShellComponent {
|
|
|
4248
4785
|
}
|
|
4249
4786
|
}
|
|
4250
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 });
|
|
4251
|
-
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"] }] });
|
|
4252
4789
|
}
|
|
4253
4790
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
4254
4791
|
type: Component,
|
|
@@ -4259,7 +4796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4259
4796
|
MnConfirmationBodyComponent,
|
|
4260
4797
|
MnCustomBodyHostComponent,
|
|
4261
4798
|
MnButton,
|
|
4262
|
-
], 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"] }]
|
|
4263
4800
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { config: [{
|
|
4264
4801
|
type: Input
|
|
4265
4802
|
}], modalRef: [{
|
|
@@ -4275,6 +4812,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4275
4812
|
class MnModalService {
|
|
4276
4813
|
appRef = inject(ApplicationRef);
|
|
4277
4814
|
injector = inject(EnvironmentInjector);
|
|
4815
|
+
modalStack = [];
|
|
4278
4816
|
open(config) {
|
|
4279
4817
|
// Create the modal shell component
|
|
4280
4818
|
const componentRef = createComponent(MnModalShellComponent, {
|
|
@@ -4285,6 +4823,12 @@ class MnModalService {
|
|
|
4285
4823
|
// Create modal ref
|
|
4286
4824
|
const modalRef = new MnModalRef(componentRef, config);
|
|
4287
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);
|
|
4288
4832
|
// Attach to application
|
|
4289
4833
|
this.appRef.attachView(componentRef.hostView);
|
|
4290
4834
|
const domElem = componentRef.location.nativeElement;
|
|
@@ -4293,6 +4837,15 @@ class MnModalService {
|
|
|
4293
4837
|
modalRef.afterClosed$.subscribe(() => {
|
|
4294
4838
|
this.appRef.detachView(componentRef.hostView);
|
|
4295
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
|
+
}
|
|
4296
4849
|
});
|
|
4297
4850
|
return modalRef;
|
|
4298
4851
|
}
|
|
@@ -4797,6 +5350,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4797
5350
|
}]
|
|
4798
5351
|
}], ctorParameters: () => [{ type: MnLanguageService }] });
|
|
4799
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
|
+
|
|
4800
5389
|
/*
|
|
4801
5390
|
* Public API Surface of mn-lib
|
|
4802
5391
|
*/
|
|
@@ -4805,5 +5394,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4805
5394
|
* Generated bundle index. Do not edit.
|
|
4806
5395
|
*/
|
|
4807
5396
|
|
|
4808
|
-
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 };
|
|
4809
5398
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|