mn-angular-lib 0.0.43 → 0.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mn-angular-lib.mjs +2959 -76
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/features/mn-modal/components/mn-confirmation-body/mn-confirmation-body.component.css +1 -0
- package/src/lib/features/mn-modal/components/mn-form-body/mn-form-body.component.css +5 -0
- package/src/lib/features/mn-modal/components/mn-modal-shell/mn-modal-shell.component.css +36 -0
- package/src/lib/features/mn-modal/components/mn-wizard-body/mn-wizard-body.component.css +1 -0
- package/src/lib/features/mn-table/mn-table.component.css +1 -0
- package/types/mn-angular-lib.d.ts +1728 -80
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, Self, APP_INITIALIZER, SkipSelf, Attribute,
|
|
3
|
-
|
|
2
|
+
import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, Self, APP_INITIALIZER, ElementRef, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewChildren, ViewContainerRef, ViewChild, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
|
|
3
|
+
export { TemplateRef, Type } from '@angular/core';
|
|
4
|
+
import { BehaviorSubject, firstValueFrom, Subject, debounceTime } from 'rxjs';
|
|
4
5
|
import * as i1 from '@angular/common';
|
|
5
|
-
import { CommonModule, NgClass, NgOptimizedImage } from '@angular/common';
|
|
6
|
+
import { CommonModule, NgClass, NgOptimizedImage, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
|
6
7
|
import { tv } from 'tailwind-variants';
|
|
7
8
|
import * as i1$2 from '@angular/forms';
|
|
8
|
-
import { Validators } from '@angular/forms';
|
|
9
|
-
import JSON5 from 'json5';
|
|
9
|
+
import { Validators, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
|
10
10
|
import * as i1$1 from '@angular/common/http';
|
|
11
|
+
import JSON5 from 'json5';
|
|
11
12
|
|
|
12
13
|
// projects/mn-angular-lib/src/lib/mn-mn-alert/mn-mn-alert.tokens.ts
|
|
13
14
|
const MN_ALERT_CONFIG = new InjectionToken('MN_ALERT_CONFIG');
|
|
@@ -546,6 +547,106 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
546
547
|
args: [{ required: true }]
|
|
547
548
|
}] } });
|
|
548
549
|
|
|
550
|
+
class MnLanguageService {
|
|
551
|
+
http;
|
|
552
|
+
_translations = {};
|
|
553
|
+
_locale$ = new BehaviorSubject('en');
|
|
554
|
+
_urlPattern = null;
|
|
555
|
+
/** Observable of the current active locale. */
|
|
556
|
+
locale$ = this._locale$.asObservable();
|
|
557
|
+
constructor(http) {
|
|
558
|
+
this.http = http;
|
|
559
|
+
}
|
|
560
|
+
/** Current active locale. */
|
|
561
|
+
get locale() {
|
|
562
|
+
return this._locale$.value;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Configure the URL pattern used to fetch translation files.
|
|
566
|
+
* Use `{locale}` as placeholder, e.g. `"assets/i18n/{locale}.json"`.
|
|
567
|
+
*/
|
|
568
|
+
configure(urlPattern) {
|
|
569
|
+
this._urlPattern = urlPattern;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Load translations for a locale from the configured URL pattern.
|
|
573
|
+
* If translations are already loaded for this locale, this is a no-op.
|
|
574
|
+
*/
|
|
575
|
+
async loadLocale(locale) {
|
|
576
|
+
if (this._translations[locale])
|
|
577
|
+
return;
|
|
578
|
+
if (!this._urlPattern) {
|
|
579
|
+
console.warn(`[MnLanguage] No URL pattern configured. Call configure() or use provideMnLanguage().`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const url = this._urlPattern.replace('{locale}', locale);
|
|
583
|
+
try {
|
|
584
|
+
const map = await firstValueFrom(this.http.get(url));
|
|
585
|
+
this._translations[locale] = map ?? {};
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
console.warn(`[MnLanguage] Failed to load translations from ${url}`, err);
|
|
589
|
+
this._translations[locale] = {};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Switch the active locale. Loads translations if not yet loaded.
|
|
594
|
+
*/
|
|
595
|
+
async setLocale(locale) {
|
|
596
|
+
await this.loadLocale(locale);
|
|
597
|
+
this._locale$.next(locale);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Register translations for a locale directly from code (no HTTP needed).
|
|
601
|
+
*/
|
|
602
|
+
registerTranslations(locale, translations) {
|
|
603
|
+
this._translations[locale] = {
|
|
604
|
+
...(this._translations[locale] ?? {}),
|
|
605
|
+
...translations,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Translate a key using the current locale, with optional parameter interpolation.
|
|
610
|
+
* Falls back to the key itself if no translation is found.
|
|
611
|
+
*
|
|
612
|
+
* Interpolation replaces `{{paramName}}` with the provided value.
|
|
613
|
+
*/
|
|
614
|
+
translate(key, params) {
|
|
615
|
+
const map = this._translations[this.locale] ?? {};
|
|
616
|
+
let value = map[key];
|
|
617
|
+
if (value === undefined) {
|
|
618
|
+
return key;
|
|
619
|
+
}
|
|
620
|
+
if (params) {
|
|
621
|
+
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
622
|
+
value = value.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, 'g'), String(paramValue));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return value;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Shorthand alias for `translate`.
|
|
629
|
+
*/
|
|
630
|
+
t(key, params) {
|
|
631
|
+
return this.translate(key, params);
|
|
632
|
+
}
|
|
633
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, deps: [{ token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
634
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, providedIn: 'root' });
|
|
635
|
+
}
|
|
636
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, decorators: [{
|
|
637
|
+
type: Injectable,
|
|
638
|
+
args: [{ providedIn: 'root' }]
|
|
639
|
+
}], ctorParameters: () => [{ type: i1$1.HttpClient }] });
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Type guard: checks whether a value is a translatable marker object.
|
|
643
|
+
*/
|
|
644
|
+
function isTranslatable(value) {
|
|
645
|
+
return (typeof value === 'object' &&
|
|
646
|
+
value !== null &&
|
|
647
|
+
typeof value.$translate === 'string');
|
|
648
|
+
}
|
|
649
|
+
|
|
549
650
|
function isPlainObject(value) {
|
|
550
651
|
return (typeof value === 'object' &&
|
|
551
652
|
value !== null &&
|
|
@@ -555,6 +656,7 @@ class MnConfigService {
|
|
|
555
656
|
http;
|
|
556
657
|
_config = null;
|
|
557
658
|
_debugMode = false;
|
|
659
|
+
lang = inject(MnLanguageService);
|
|
558
660
|
constructor(http) {
|
|
559
661
|
this.http = http;
|
|
560
662
|
}
|
|
@@ -585,6 +687,17 @@ class MnConfigService {
|
|
|
585
687
|
const defaults = isPlainObject(cfg.defaults) ? cfg.defaults : {};
|
|
586
688
|
const overrides = isPlainObject(cfg.overrides) ? cfg.overrides : cfg.overrides ?? {};
|
|
587
689
|
this._config = { defaults, overrides };
|
|
690
|
+
// Bootstrap language service from config if a "language" section is present.
|
|
691
|
+
// This avoids circular dependency: config reads raw language settings and
|
|
692
|
+
// pushes them into the language service (language service never imports config).
|
|
693
|
+
const langCfg = cfg.language;
|
|
694
|
+
if (isPlainObject(langCfg) && typeof langCfg['urlPattern'] === 'string') {
|
|
695
|
+
const lc = langCfg;
|
|
696
|
+
this.lang.configure(lc.urlPattern);
|
|
697
|
+
const localesToLoad = lc.preload ?? [lc.defaultLocale];
|
|
698
|
+
await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
|
|
699
|
+
await this.lang.setLocale(lc.defaultLocale);
|
|
700
|
+
}
|
|
588
701
|
}
|
|
589
702
|
/**
|
|
590
703
|
* Resolve a configuration object for a component, optionally scoped to a section path
|
|
@@ -614,7 +727,7 @@ class MnConfigService {
|
|
|
614
727
|
resolved,
|
|
615
728
|
});
|
|
616
729
|
}
|
|
617
|
-
return resolved;
|
|
730
|
+
return this.resolveTranslatables(resolved);
|
|
618
731
|
}
|
|
619
732
|
/**
|
|
620
733
|
* Walk the overrides nested object using the provided section path and return the leaf node.
|
|
@@ -631,6 +744,25 @@ class MnConfigService {
|
|
|
631
744
|
}
|
|
632
745
|
return node;
|
|
633
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Recursively walk a resolved config object and replace any `{ $translate: "key" }` markers
|
|
749
|
+
* with their translated values from MnLanguageService.
|
|
750
|
+
*/
|
|
751
|
+
resolveTranslatables(obj) {
|
|
752
|
+
const out = {};
|
|
753
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
754
|
+
if (isTranslatable(value)) {
|
|
755
|
+
out[key] = this.lang.translate(value.$translate, value.params);
|
|
756
|
+
}
|
|
757
|
+
else if (isPlainObject(value)) {
|
|
758
|
+
out[key] = this.resolveTranslatables(value);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
out[key] = value;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
634
766
|
/**
|
|
635
767
|
* Deep merge two plain-object trees. Arrays and non-plain values are replaced by the patch.
|
|
636
768
|
* Does not mutate inputs; returns a new object.
|
|
@@ -752,6 +884,13 @@ class MnInputField {
|
|
|
752
884
|
resolveConfig() {
|
|
753
885
|
const instanceId = this.explicitInstanceId || `mn-input-${this.props.id}`;
|
|
754
886
|
this.uiConfig = this.configService.resolve('mn-input-field', this.sectionPath, instanceId);
|
|
887
|
+
// Allow props to override uiConfig for label and placeholder
|
|
888
|
+
if (this.props.label) {
|
|
889
|
+
this.uiConfig = { ...this.uiConfig, label: this.props.label };
|
|
890
|
+
}
|
|
891
|
+
if (this.props.placeholder) {
|
|
892
|
+
this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
|
|
893
|
+
}
|
|
755
894
|
}
|
|
756
895
|
/**
|
|
757
896
|
* Gets the appropriate adapter based on the input type.
|
|
@@ -1391,80 +1530,2824 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1391
1530
|
args: [{ required: true }]
|
|
1392
1531
|
}] } });
|
|
1393
1532
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
selector: '[mn-section]',
|
|
1416
|
-
standalone: true,
|
|
1417
|
-
providers: [
|
|
1418
|
-
{
|
|
1419
|
-
provide: MN_SECTION_PATH,
|
|
1420
|
-
// Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
|
|
1421
|
-
// and read the attribute value using Attribute so it's available at provider creation time.
|
|
1422
|
-
deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
|
|
1423
|
-
useFactory: (parentPath, attr) => {
|
|
1424
|
-
const parent = Array.isArray(parentPath) ? parentPath : [];
|
|
1425
|
-
const name = (attr ?? '').trim();
|
|
1426
|
-
return name ? [...parent, name] : [...parent];
|
|
1427
|
-
},
|
|
1428
|
-
},
|
|
1429
|
-
],
|
|
1430
|
-
}]
|
|
1431
|
-
}], propDecorators: { mnSection: [{
|
|
1432
|
-
type: Input,
|
|
1433
|
-
args: ['mn-section']
|
|
1434
|
-
}] } });
|
|
1533
|
+
const mnCheckboxVariants = tv({
|
|
1534
|
+
base: 'accent-brand-500 cursor-pointer',
|
|
1535
|
+
variants: {
|
|
1536
|
+
size: {
|
|
1537
|
+
sm: 'w-4 h-4',
|
|
1538
|
+
md: 'w-5 h-5',
|
|
1539
|
+
lg: 'w-6 h-6',
|
|
1540
|
+
},
|
|
1541
|
+
borderRadius: {
|
|
1542
|
+
none: 'rounded-none',
|
|
1543
|
+
xs: 'rounded-xs',
|
|
1544
|
+
sm: 'rounded-sm',
|
|
1545
|
+
md: 'rounded-md',
|
|
1546
|
+
lg: 'rounded-lg',
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
defaultVariants: {
|
|
1550
|
+
size: 'md',
|
|
1551
|
+
borderRadius: 'sm',
|
|
1552
|
+
},
|
|
1553
|
+
});
|
|
1435
1554
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1555
|
+
const MN_CHECKBOX_CONFIG = new InjectionToken('MN_CHECKBOX_CONFIG');
|
|
1556
|
+
class MnCheckbox {
|
|
1557
|
+
ngControl;
|
|
1558
|
+
uiConfig = {};
|
|
1559
|
+
props;
|
|
1560
|
+
configService = inject(MnConfigService);
|
|
1561
|
+
sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
|
|
1562
|
+
explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
|
|
1563
|
+
value = false;
|
|
1564
|
+
isDisabled = false;
|
|
1565
|
+
onChange = () => { };
|
|
1566
|
+
onTouched = () => { };
|
|
1567
|
+
builtInErrorMessages = {
|
|
1568
|
+
required: 'This field is required',
|
|
1569
|
+
};
|
|
1570
|
+
constructor(ngControl) {
|
|
1571
|
+
this.ngControl = ngControl;
|
|
1572
|
+
if (this.ngControl)
|
|
1573
|
+
this.ngControl.valueAccessor = this;
|
|
1574
|
+
}
|
|
1575
|
+
ngOnInit() {
|
|
1576
|
+
this.resolveConfig();
|
|
1577
|
+
}
|
|
1578
|
+
resolveConfig() {
|
|
1579
|
+
const instanceId = this.explicitInstanceId || `mn-checkbox-${this.props.id}`;
|
|
1580
|
+
this.uiConfig = this.configService.resolve('mn-checkbox', this.sectionPath, instanceId);
|
|
1581
|
+
if (this.props.label) {
|
|
1582
|
+
this.uiConfig = { ...this.uiConfig, label: this.props.label };
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
// ========== ControlValueAccessor Implementation ==========
|
|
1586
|
+
writeValue(val) {
|
|
1587
|
+
this.value = !!val;
|
|
1588
|
+
}
|
|
1589
|
+
registerOnChange(fn) {
|
|
1590
|
+
this.onChange = fn;
|
|
1591
|
+
}
|
|
1592
|
+
registerOnTouched(fn) {
|
|
1593
|
+
this.onTouched = fn;
|
|
1594
|
+
}
|
|
1595
|
+
setDisabledState(isDisabled) {
|
|
1596
|
+
this.isDisabled = isDisabled;
|
|
1597
|
+
}
|
|
1598
|
+
// ========== Event Handlers ==========
|
|
1599
|
+
handleChange(checked) {
|
|
1600
|
+
this.value = checked;
|
|
1601
|
+
this.onChange(checked);
|
|
1602
|
+
}
|
|
1603
|
+
handleBlur() {
|
|
1604
|
+
this.onTouched();
|
|
1605
|
+
}
|
|
1606
|
+
// ========== Error Handling ==========
|
|
1607
|
+
get control() {
|
|
1608
|
+
return this.ngControl?.control ?? null;
|
|
1609
|
+
}
|
|
1610
|
+
get showError() {
|
|
1611
|
+
const c = this.control;
|
|
1612
|
+
return !!c && c.invalid && (c.touched || c.dirty);
|
|
1613
|
+
}
|
|
1614
|
+
pickErrorKey(errors) {
|
|
1615
|
+
if (this.props.errorPriority) {
|
|
1616
|
+
for (const key of this.props.errorPriority) {
|
|
1617
|
+
if (errors[key] !== undefined) {
|
|
1618
|
+
return key;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return Object.keys(errors)[0];
|
|
1623
|
+
}
|
|
1624
|
+
isRequired() {
|
|
1625
|
+
if (!this.control)
|
|
1626
|
+
return false;
|
|
1627
|
+
return this.control.hasValidator(Validators.required);
|
|
1628
|
+
}
|
|
1629
|
+
resolveErrorMessageForKey(errorKey, errors) {
|
|
1630
|
+
const errorArgs = errors[errorKey];
|
|
1631
|
+
const customMsg = this.props.errorMessages?.[errorKey];
|
|
1632
|
+
const useBuiltIn = this.props.useBuiltInErrorMessages !== false;
|
|
1633
|
+
const builtInMsg = useBuiltIn ? this.builtInErrorMessages[errorKey] : undefined;
|
|
1634
|
+
const fallbackMsg = this.props.defaultErrorMessage;
|
|
1635
|
+
const msgDef = customMsg ?? builtInMsg ?? fallbackMsg ?? 'Invalid input';
|
|
1636
|
+
if (typeof msgDef === 'function') {
|
|
1637
|
+
return msgDef(errorArgs, errors);
|
|
1638
|
+
}
|
|
1639
|
+
return msgDef;
|
|
1640
|
+
}
|
|
1641
|
+
get errorMessages() {
|
|
1642
|
+
const errors = this.control?.errors;
|
|
1643
|
+
if (!errors)
|
|
1644
|
+
return [];
|
|
1645
|
+
return Object.keys(errors).map(key => this.resolveErrorMessageForKey(key, errors));
|
|
1646
|
+
}
|
|
1647
|
+
get errorMessage() {
|
|
1648
|
+
const errors = this.control?.errors;
|
|
1649
|
+
if (!errors)
|
|
1650
|
+
return null;
|
|
1651
|
+
const errorKey = this.pickErrorKey(errors);
|
|
1652
|
+
return this.resolveErrorMessageForKey(errorKey, errors);
|
|
1653
|
+
}
|
|
1654
|
+
// ========== Resolved Properties ==========
|
|
1655
|
+
get resolvedId() {
|
|
1656
|
+
return this.props.id;
|
|
1657
|
+
}
|
|
1658
|
+
get resolvedName() {
|
|
1659
|
+
return this.props?.name ?? null;
|
|
1660
|
+
}
|
|
1661
|
+
get checkboxClasses() {
|
|
1662
|
+
return mnCheckboxVariants({
|
|
1663
|
+
size: this.props.size,
|
|
1664
|
+
borderRadius: this.props.borderRadius,
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCheckbox, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
1668
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCheckbox, isStandalone: true, selector: "mn-lib-checkbox", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col\">\n <label class=\"flex flex-row items-center gap-x-2 cursor-pointer\" [attr.for]=\"resolvedId\">\n <input\n type=\"checkbox\"\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [checked]=\"value\"\n [ngClass]=\"checkboxClasses\"\n (change)=\"handleChange($any($event.target).checked)\"\n (blur)=\"handleBlur()\"\n />\n @if (uiConfig.label || props.label) {\n <span class=\"text-sm text-gray-700 select-none\">{{ uiConfig.label || props.label }}</span>\n }\n </label>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
1448
1669
|
}
|
|
1449
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type:
|
|
1450
|
-
type:
|
|
1451
|
-
args: [{
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
// Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
|
|
1458
|
-
deps: [new Attribute('mn-instance')],
|
|
1459
|
-
useFactory: (attr) => (attr ?? '').trim() || null,
|
|
1460
|
-
},
|
|
1461
|
-
],
|
|
1462
|
-
}]
|
|
1463
|
-
}], propDecorators: { mnInstance: [{
|
|
1670
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCheckbox, decorators: [{
|
|
1671
|
+
type: Component,
|
|
1672
|
+
args: [{ selector: 'mn-lib-checkbox', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col\">\n <label class=\"flex flex-row items-center gap-x-2 cursor-pointer\" [attr.for]=\"resolvedId\">\n <input\n type=\"checkbox\"\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [checked]=\"value\"\n [ngClass]=\"checkboxClasses\"\n (change)=\"handleChange($any($event.target).checked)\"\n (blur)=\"handleBlur()\"\n />\n @if (uiConfig.label || props.label) {\n <span class=\"text-sm text-gray-700 select-none\">{{ uiConfig.label || props.label }}</span>\n }\n </label>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
1673
|
+
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
1674
|
+
type: Optional
|
|
1675
|
+
}, {
|
|
1676
|
+
type: Self
|
|
1677
|
+
}] }], propDecorators: { props: [{
|
|
1464
1678
|
type: Input,
|
|
1465
|
-
args: [
|
|
1679
|
+
args: [{ required: true }]
|
|
1466
1680
|
}] } });
|
|
1467
1681
|
|
|
1682
|
+
const mnDatetimeVariants = tv({
|
|
1683
|
+
base: 'bg-white border-1 border-gray-500 placeholder-gray-500 text-sm',
|
|
1684
|
+
variants: {
|
|
1685
|
+
shadow: {
|
|
1686
|
+
true: 'shadow-lg',
|
|
1687
|
+
},
|
|
1688
|
+
size: {
|
|
1689
|
+
sm: 'p-2',
|
|
1690
|
+
md: 'p-3',
|
|
1691
|
+
lg: 'p-4',
|
|
1692
|
+
},
|
|
1693
|
+
borderRadius: {
|
|
1694
|
+
none: 'rounded-none',
|
|
1695
|
+
xs: 'rounded-xs',
|
|
1696
|
+
sm: 'rounded-sm',
|
|
1697
|
+
md: 'rounded-md',
|
|
1698
|
+
lg: 'rounded-lg',
|
|
1699
|
+
xl: 'rounded-xl',
|
|
1700
|
+
two_xl: 'rounded-2xl',
|
|
1701
|
+
three_xl: 'rounded-3xl',
|
|
1702
|
+
four_xl: 'rounded-4xl',
|
|
1703
|
+
},
|
|
1704
|
+
fullWidth: {
|
|
1705
|
+
true: 'w-full',
|
|
1706
|
+
},
|
|
1707
|
+
},
|
|
1708
|
+
defaultVariants: {
|
|
1709
|
+
size: 'md',
|
|
1710
|
+
borderRadius: 'md',
|
|
1711
|
+
},
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
const MN_DATETIME_CONFIG = new InjectionToken('MN_DATETIME_CONFIG');
|
|
1715
|
+
class MnDatetime {
|
|
1716
|
+
ngControl;
|
|
1717
|
+
uiConfig = {};
|
|
1718
|
+
props;
|
|
1719
|
+
configService = inject(MnConfigService);
|
|
1720
|
+
sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
|
|
1721
|
+
explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
|
|
1722
|
+
value = null;
|
|
1723
|
+
isDisabled = false;
|
|
1724
|
+
onChange = () => { };
|
|
1725
|
+
onTouched = () => { };
|
|
1726
|
+
builtInErrorMessages = {
|
|
1727
|
+
required: 'This field is required',
|
|
1728
|
+
mnMin: (args) => `Date/time must be from ${args.min} onwards`,
|
|
1729
|
+
mnMax: (args) => `Date/time must be up to ${args.max}`,
|
|
1730
|
+
};
|
|
1731
|
+
constructor(ngControl) {
|
|
1732
|
+
this.ngControl = ngControl;
|
|
1733
|
+
if (this.ngControl)
|
|
1734
|
+
this.ngControl.valueAccessor = this;
|
|
1735
|
+
}
|
|
1736
|
+
ngOnInit() {
|
|
1737
|
+
this.resolveConfig();
|
|
1738
|
+
}
|
|
1739
|
+
resolveConfig() {
|
|
1740
|
+
const instanceId = this.explicitInstanceId || `mn-datetime-${this.props.id}`;
|
|
1741
|
+
this.uiConfig = this.configService.resolve('mn-datetime', this.sectionPath, instanceId);
|
|
1742
|
+
if (this.props.label) {
|
|
1743
|
+
this.uiConfig = { ...this.uiConfig, label: this.props.label };
|
|
1744
|
+
}
|
|
1745
|
+
if (this.props.placeholder) {
|
|
1746
|
+
this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
// ========== ControlValueAccessor Implementation ==========
|
|
1750
|
+
writeValue(val) {
|
|
1751
|
+
this.value = val != null ? String(val) : null;
|
|
1752
|
+
}
|
|
1753
|
+
registerOnChange(fn) {
|
|
1754
|
+
this.onChange = fn;
|
|
1755
|
+
}
|
|
1756
|
+
registerOnTouched(fn) {
|
|
1757
|
+
this.onTouched = fn;
|
|
1758
|
+
}
|
|
1759
|
+
setDisabledState(isDisabled) {
|
|
1760
|
+
this.isDisabled = isDisabled;
|
|
1761
|
+
}
|
|
1762
|
+
// ========== Event Handlers ==========
|
|
1763
|
+
handleInput(raw) {
|
|
1764
|
+
this.value = raw;
|
|
1765
|
+
this.onChange(raw);
|
|
1766
|
+
}
|
|
1767
|
+
handleBlur() {
|
|
1768
|
+
this.onTouched();
|
|
1769
|
+
}
|
|
1770
|
+
// ========== Error Handling ==========
|
|
1771
|
+
get control() {
|
|
1772
|
+
return this.ngControl?.control ?? null;
|
|
1773
|
+
}
|
|
1774
|
+
get showError() {
|
|
1775
|
+
const c = this.control;
|
|
1776
|
+
return !!c && c.invalid && (c.touched || c.dirty);
|
|
1777
|
+
}
|
|
1778
|
+
pickErrorKey(errors) {
|
|
1779
|
+
if (this.props.errorPriority) {
|
|
1780
|
+
for (const key of this.props.errorPriority) {
|
|
1781
|
+
if (errors[key] !== undefined) {
|
|
1782
|
+
return key;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return Object.keys(errors)[0];
|
|
1787
|
+
}
|
|
1788
|
+
isRequired() {
|
|
1789
|
+
if (!this.control)
|
|
1790
|
+
return false;
|
|
1791
|
+
return this.control.hasValidator(Validators.required);
|
|
1792
|
+
}
|
|
1793
|
+
resolveErrorMessageForKey(errorKey, errors) {
|
|
1794
|
+
const errorArgs = errors[errorKey];
|
|
1795
|
+
const customMsg = this.props.errorMessages?.[errorKey];
|
|
1796
|
+
const useBuiltIn = this.props.useBuiltInErrorMessages !== false;
|
|
1797
|
+
const builtInMsg = useBuiltIn ? this.builtInErrorMessages[errorKey] : undefined;
|
|
1798
|
+
const fallbackMsg = this.props.defaultErrorMessage;
|
|
1799
|
+
const msgDef = customMsg ?? builtInMsg ?? fallbackMsg ?? 'Invalid input';
|
|
1800
|
+
if (typeof msgDef === 'function') {
|
|
1801
|
+
return msgDef(errorArgs, errors);
|
|
1802
|
+
}
|
|
1803
|
+
return msgDef;
|
|
1804
|
+
}
|
|
1805
|
+
get errorMessages() {
|
|
1806
|
+
const errors = this.control?.errors;
|
|
1807
|
+
if (!errors)
|
|
1808
|
+
return [];
|
|
1809
|
+
return Object.keys(errors).map(key => this.resolveErrorMessageForKey(key, errors));
|
|
1810
|
+
}
|
|
1811
|
+
get errorMessage() {
|
|
1812
|
+
const errors = this.control?.errors;
|
|
1813
|
+
if (!errors)
|
|
1814
|
+
return null;
|
|
1815
|
+
const errorKey = this.pickErrorKey(errors);
|
|
1816
|
+
return this.resolveErrorMessageForKey(errorKey, errors);
|
|
1817
|
+
}
|
|
1818
|
+
// ========== Resolved Properties ==========
|
|
1819
|
+
get resolvedId() {
|
|
1820
|
+
return this.props.id;
|
|
1821
|
+
}
|
|
1822
|
+
get resolvedName() {
|
|
1823
|
+
return this.props?.name ?? null;
|
|
1824
|
+
}
|
|
1825
|
+
get resolvedMode() {
|
|
1826
|
+
return this.props.mode ?? 'datetime-local';
|
|
1827
|
+
}
|
|
1828
|
+
get inputClasses() {
|
|
1829
|
+
return mnDatetimeVariants({
|
|
1830
|
+
size: this.props.size,
|
|
1831
|
+
borderRadius: this.props.borderRadius,
|
|
1832
|
+
shadow: this.props.shadow,
|
|
1833
|
+
fullWidth: this.props.fullWidth,
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDatetime, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
1837
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDatetime, isStandalone: true, selector: "mn-lib-datetime", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <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"] }] });
|
|
1838
|
+
}
|
|
1839
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDatetime, decorators: [{
|
|
1840
|
+
type: Component,
|
|
1841
|
+
args: [{ selector: 'mn-lib-datetime', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"resolvedMode\"\n [attr.placeholder]=\"uiConfig.placeholder || props.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"props.min || null\"\n [attr.max]=\"props.max || null\"\n [attr.step]=\"props.step || null\"\n [value]=\"value ?? ''\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <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" }]
|
|
1842
|
+
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
1843
|
+
type: Optional
|
|
1844
|
+
}, {
|
|
1845
|
+
type: Self
|
|
1846
|
+
}] }], propDecorators: { props: [{
|
|
1847
|
+
type: Input,
|
|
1848
|
+
args: [{ required: true }]
|
|
1849
|
+
}] } });
|
|
1850
|
+
|
|
1851
|
+
const mnMultiSelectVariants = tv({
|
|
1852
|
+
base: 'bg-white border-1 border-gray-500 text-sm cursor-pointer',
|
|
1853
|
+
variants: {
|
|
1854
|
+
shadow: {
|
|
1855
|
+
true: 'shadow-lg',
|
|
1856
|
+
},
|
|
1857
|
+
size: {
|
|
1858
|
+
sm: 'p-2',
|
|
1859
|
+
md: 'p-3',
|
|
1860
|
+
lg: 'p-4',
|
|
1861
|
+
},
|
|
1862
|
+
borderRadius: {
|
|
1863
|
+
none: 'rounded-none',
|
|
1864
|
+
xs: 'rounded-xs',
|
|
1865
|
+
sm: 'rounded-sm',
|
|
1866
|
+
md: 'rounded-md',
|
|
1867
|
+
lg: 'rounded-lg',
|
|
1868
|
+
xl: 'rounded-xl',
|
|
1869
|
+
two_xl: 'rounded-2xl',
|
|
1870
|
+
three_xl: 'rounded-3xl',
|
|
1871
|
+
four_xl: 'rounded-4xl',
|
|
1872
|
+
},
|
|
1873
|
+
fullWidth: {
|
|
1874
|
+
true: 'w-full',
|
|
1875
|
+
},
|
|
1876
|
+
},
|
|
1877
|
+
defaultVariants: {
|
|
1878
|
+
size: 'md',
|
|
1879
|
+
borderRadius: 'md',
|
|
1880
|
+
},
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
const MN_MULTI_SELECT_CONFIG = new InjectionToken('MN_MULTI_SELECT_CONFIG');
|
|
1884
|
+
class MnMultiSelect {
|
|
1885
|
+
ngControl;
|
|
1886
|
+
uiConfig = {};
|
|
1887
|
+
props;
|
|
1888
|
+
configService = inject(MnConfigService);
|
|
1889
|
+
sectionPath = inject(MN_SECTION_PATH, { optional: true }) ?? [];
|
|
1890
|
+
explicitInstanceId = inject(MN_INSTANCE_ID, { optional: true });
|
|
1891
|
+
elRef = inject(ElementRef);
|
|
1892
|
+
/** Currently selected values */
|
|
1893
|
+
selectedValues = [];
|
|
1894
|
+
isOpen = false;
|
|
1895
|
+
isDisabled = false;
|
|
1896
|
+
searchTerm = '';
|
|
1897
|
+
onChange = () => { };
|
|
1898
|
+
onTouched = () => { };
|
|
1899
|
+
builtInErrorMessages = {
|
|
1900
|
+
required: 'At least one option must be selected',
|
|
1901
|
+
};
|
|
1902
|
+
constructor(ngControl) {
|
|
1903
|
+
this.ngControl = ngControl;
|
|
1904
|
+
if (this.ngControl)
|
|
1905
|
+
this.ngControl.valueAccessor = this;
|
|
1906
|
+
}
|
|
1907
|
+
ngOnInit() {
|
|
1908
|
+
this.resolveConfig();
|
|
1909
|
+
}
|
|
1910
|
+
resolveConfig() {
|
|
1911
|
+
const instanceId = this.explicitInstanceId || `mn-multi-select-${this.props.id}`;
|
|
1912
|
+
this.uiConfig = this.configService.resolve('mn-multi-select', this.sectionPath, instanceId);
|
|
1913
|
+
if (this.props.label) {
|
|
1914
|
+
this.uiConfig = { ...this.uiConfig, label: this.props.label };
|
|
1915
|
+
}
|
|
1916
|
+
if (this.props.placeholder) {
|
|
1917
|
+
this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
// ========== ControlValueAccessor Implementation ==========
|
|
1921
|
+
writeValue(val) {
|
|
1922
|
+
this.selectedValues = Array.isArray(val) ? val : [];
|
|
1923
|
+
}
|
|
1924
|
+
registerOnChange(fn) {
|
|
1925
|
+
this.onChange = fn;
|
|
1926
|
+
}
|
|
1927
|
+
registerOnTouched(fn) {
|
|
1928
|
+
this.onTouched = fn;
|
|
1929
|
+
}
|
|
1930
|
+
setDisabledState(isDisabled) {
|
|
1931
|
+
this.isDisabled = isDisabled;
|
|
1932
|
+
}
|
|
1933
|
+
// ========== Dropdown Logic ==========
|
|
1934
|
+
toggle() {
|
|
1935
|
+
if (this.isDisabled)
|
|
1936
|
+
return;
|
|
1937
|
+
this.isOpen = !this.isOpen;
|
|
1938
|
+
if (!this.isOpen) {
|
|
1939
|
+
this.searchTerm = '';
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
onDocumentClick(event) {
|
|
1943
|
+
if (!this.elRef.nativeElement.contains(event.target)) {
|
|
1944
|
+
if (this.isOpen) {
|
|
1945
|
+
this.isOpen = false;
|
|
1946
|
+
this.searchTerm = '';
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
toggleOption(option) {
|
|
1951
|
+
if (option.disabled)
|
|
1952
|
+
return;
|
|
1953
|
+
const index = this.selectedValues.indexOf(option.value);
|
|
1954
|
+
if (index > -1) {
|
|
1955
|
+
this.selectedValues = this.selectedValues.filter(v => v !== option.value);
|
|
1956
|
+
}
|
|
1957
|
+
else {
|
|
1958
|
+
if (this.props.maxSelections && this.selectedValues.length >= this.props.maxSelections) {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
this.selectedValues = [...this.selectedValues, option.value];
|
|
1962
|
+
}
|
|
1963
|
+
this.onChange(this.selectedValues);
|
|
1964
|
+
}
|
|
1965
|
+
removeOption(option, event) {
|
|
1966
|
+
event.stopPropagation();
|
|
1967
|
+
this.selectedValues = this.selectedValues.filter(v => v !== option.value);
|
|
1968
|
+
this.onChange(this.selectedValues);
|
|
1969
|
+
}
|
|
1970
|
+
isSelected(option) {
|
|
1971
|
+
return this.selectedValues.includes(option.value);
|
|
1972
|
+
}
|
|
1973
|
+
isMaxReached(option) {
|
|
1974
|
+
if (!this.props.maxSelections)
|
|
1975
|
+
return false;
|
|
1976
|
+
return this.selectedValues.length >= this.props.maxSelections && !this.isSelected(option);
|
|
1977
|
+
}
|
|
1978
|
+
onSearch(term) {
|
|
1979
|
+
this.searchTerm = term;
|
|
1980
|
+
}
|
|
1981
|
+
get filteredOptions() {
|
|
1982
|
+
if (!this.searchTerm)
|
|
1983
|
+
return this.props.options;
|
|
1984
|
+
const lower = this.searchTerm.toLowerCase();
|
|
1985
|
+
return this.props.options.filter(o => o.label.toLowerCase().includes(lower));
|
|
1986
|
+
}
|
|
1987
|
+
get selectedOptions() {
|
|
1988
|
+
return this.props.options.filter(o => this.selectedValues.includes(o.value));
|
|
1989
|
+
}
|
|
1990
|
+
handleBlur() {
|
|
1991
|
+
this.onTouched();
|
|
1992
|
+
}
|
|
1993
|
+
// ========== Error Handling ==========
|
|
1994
|
+
get control() {
|
|
1995
|
+
return this.ngControl?.control ?? null;
|
|
1996
|
+
}
|
|
1997
|
+
get showError() {
|
|
1998
|
+
const c = this.control;
|
|
1999
|
+
return !!c && c.invalid && (c.touched || c.dirty);
|
|
2000
|
+
}
|
|
2001
|
+
pickErrorKey(errors) {
|
|
2002
|
+
if (this.props.errorPriority) {
|
|
2003
|
+
for (const key of this.props.errorPriority) {
|
|
2004
|
+
if (errors[key] !== undefined) {
|
|
2005
|
+
return key;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return Object.keys(errors)[0];
|
|
2010
|
+
}
|
|
2011
|
+
isRequired() {
|
|
2012
|
+
if (!this.control)
|
|
2013
|
+
return false;
|
|
2014
|
+
return this.control.hasValidator(Validators.required);
|
|
2015
|
+
}
|
|
2016
|
+
resolveErrorMessageForKey(errorKey, errors) {
|
|
2017
|
+
const errorArgs = errors[errorKey];
|
|
2018
|
+
const customMsg = this.props.errorMessages?.[errorKey];
|
|
2019
|
+
const useBuiltIn = this.props.useBuiltInErrorMessages !== false;
|
|
2020
|
+
const builtInMsg = useBuiltIn ? this.builtInErrorMessages[errorKey] : undefined;
|
|
2021
|
+
const fallbackMsg = this.props.defaultErrorMessage;
|
|
2022
|
+
const msgDef = customMsg ?? builtInMsg ?? fallbackMsg ?? 'Invalid input';
|
|
2023
|
+
if (typeof msgDef === 'function') {
|
|
2024
|
+
return msgDef(errorArgs, errors);
|
|
2025
|
+
}
|
|
2026
|
+
return msgDef;
|
|
2027
|
+
}
|
|
2028
|
+
get errorMessages() {
|
|
2029
|
+
const errors = this.control?.errors;
|
|
2030
|
+
if (!errors)
|
|
2031
|
+
return [];
|
|
2032
|
+
return Object.keys(errors).map(key => this.resolveErrorMessageForKey(key, errors));
|
|
2033
|
+
}
|
|
2034
|
+
get errorMessage() {
|
|
2035
|
+
const errors = this.control?.errors;
|
|
2036
|
+
if (!errors)
|
|
2037
|
+
return null;
|
|
2038
|
+
const errorKey = this.pickErrorKey(errors);
|
|
2039
|
+
return this.resolveErrorMessageForKey(errorKey, errors);
|
|
2040
|
+
}
|
|
2041
|
+
// ========== Resolved Properties ==========
|
|
2042
|
+
get resolvedId() {
|
|
2043
|
+
return this.props.id;
|
|
2044
|
+
}
|
|
2045
|
+
get resolvedName() {
|
|
2046
|
+
return this.props?.name ?? null;
|
|
2047
|
+
}
|
|
2048
|
+
get triggerClasses() {
|
|
2049
|
+
return mnMultiSelectVariants({
|
|
2050
|
+
size: this.props.size,
|
|
2051
|
+
borderRadius: this.props.borderRadius,
|
|
2052
|
+
shadow: this.props.shadow,
|
|
2053
|
+
fullWidth: this.props.fullWidth,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
2057
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <div class=\"flex flex-row items-center gap-x-1 flex-wrap min-h-[1.5rem]\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-gray-500\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <span class=\"inline-flex items-center gap-x-1 bg-gray-100 text-gray-700 text-xs px-2 py-0.5 rounded-md\">\n {{ opt.label }}\n <button\n type=\"button\"\n class=\"text-gray-400 hover:text-gray-600 cursor-pointer\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n >\u00D7</button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 12 12\">\n <path fill=\"currentColor\" d=\"M6 8L1 3h10z\"/>\n </svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n <div class=\"relative z-50\">\n <div class=\"absolute top-0 left-0 right-0 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto\">\n @if (props.searchable) {\n <div class=\"p-2 border-b border-gray-200\">\n <input\n type=\"text\"\n class=\"w-full p-1.5 text-sm border border-gray-300 rounded-md outline-none focus:border-blue-500\"\n [placeholder]=\"props.searchPlaceholder || 'Search...'\"\n [value]=\"searchTerm\"\n (input)=\"onSearch(($any($event.target)).value)\"\n (click)=\"$event.stopPropagation()\"\n />\n </div>\n }\n @for (opt of filteredOptions; track opt.value) {\n <div\n class=\"flex items-center gap-x-2 px-3 py-2 text-sm cursor-pointer hover:bg-gray-50\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n >\n <input\n type=\"checkbox\"\n class=\"w-4 h-4 accent-brand-500 pointer-events-none\"\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n tabindex=\"-1\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div class=\"px-3 py-2 text-sm text-gray-400\">No options found</div>\n }\n </div>\n </div>\n }\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <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"] }] });
|
|
2058
|
+
}
|
|
2059
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, decorators: [{
|
|
2060
|
+
type: Component,
|
|
2061
|
+
args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <div class=\"flex flex-row items-center gap-x-1 flex-wrap min-h-[1.5rem]\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-gray-500\">{{ uiConfig.placeholder || props.placeholder || 'Select...' }}</span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <span class=\"inline-flex items-center gap-x-1 bg-gray-100 text-gray-700 text-xs px-2 py-0.5 rounded-md\">\n {{ opt.label }}\n <button\n type=\"button\"\n class=\"text-gray-400 hover:text-gray-600 cursor-pointer\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n >\u00D7</button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 12 12\">\n <path fill=\"currentColor\" d=\"M6 8L1 3h10z\"/>\n </svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n <div class=\"relative z-50\">\n <div class=\"absolute top-0 left-0 right-0 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto\">\n @if (props.searchable) {\n <div class=\"p-2 border-b border-gray-200\">\n <input\n type=\"text\"\n class=\"w-full p-1.5 text-sm border border-gray-300 rounded-md outline-none focus:border-blue-500\"\n [placeholder]=\"props.searchPlaceholder || 'Search...'\"\n [value]=\"searchTerm\"\n (input)=\"onSearch(($any($event.target)).value)\"\n (click)=\"$event.stopPropagation()\"\n />\n </div>\n }\n @for (opt of filteredOptions; track opt.value) {\n <div\n class=\"flex items-center gap-x-2 px-3 py-2 text-sm cursor-pointer hover:bg-gray-50\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n >\n <input\n type=\"checkbox\"\n class=\"w-4 h-4 accent-brand-500 pointer-events-none\"\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n tabindex=\"-1\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div class=\"px-3 py-2 text-sm text-gray-400\">No options found</div>\n }\n </div>\n </div>\n }\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <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" }]
|
|
2062
|
+
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
2063
|
+
type: Optional
|
|
2064
|
+
}, {
|
|
2065
|
+
type: Self
|
|
2066
|
+
}] }], propDecorators: { props: [{
|
|
2067
|
+
type: Input,
|
|
2068
|
+
args: [{ required: true }]
|
|
2069
|
+
}], onDocumentClick: [{
|
|
2070
|
+
type: HostListener,
|
|
2071
|
+
args: ['document:click', ['$event']]
|
|
2072
|
+
}] } });
|
|
2073
|
+
|
|
2074
|
+
// =========================
|
|
2075
|
+
// Enums / Value Types
|
|
2076
|
+
// =========================
|
|
2077
|
+
var ModalKind;
|
|
2078
|
+
(function (ModalKind) {
|
|
2079
|
+
ModalKind["WIZARD"] = "wizard";
|
|
2080
|
+
ModalKind["FORM"] = "form";
|
|
2081
|
+
ModalKind["CONFIRMATION"] = "confirmation";
|
|
2082
|
+
ModalKind["CUSTOM"] = "custom";
|
|
2083
|
+
})(ModalKind || (ModalKind = {}));
|
|
2084
|
+
var ModalSize;
|
|
2085
|
+
(function (ModalSize) {
|
|
2086
|
+
ModalSize["SM"] = "sm";
|
|
2087
|
+
ModalSize["MD"] = "md";
|
|
2088
|
+
ModalSize["LG"] = "lg";
|
|
2089
|
+
ModalSize["XL"] = "xl";
|
|
2090
|
+
ModalSize["FULL"] = "full";
|
|
2091
|
+
})(ModalSize || (ModalSize = {}));
|
|
2092
|
+
var CloseMode;
|
|
2093
|
+
(function (CloseMode) {
|
|
2094
|
+
CloseMode["ALLOWED"] = "allowed";
|
|
2095
|
+
CloseMode["GUARDED"] = "guarded";
|
|
2096
|
+
CloseMode["DISABLED"] = "disabled";
|
|
2097
|
+
})(CloseMode || (CloseMode = {}));
|
|
2098
|
+
var BackdropMode;
|
|
2099
|
+
(function (BackdropMode) {
|
|
2100
|
+
BackdropMode["HIDE"] = "hide";
|
|
2101
|
+
BackdropMode["STATIC"] = "static";
|
|
2102
|
+
BackdropMode["CLOSABLE"] = "closable";
|
|
2103
|
+
})(BackdropMode || (BackdropMode = {}));
|
|
2104
|
+
var KeyboardMode;
|
|
2105
|
+
(function (KeyboardMode) {
|
|
2106
|
+
KeyboardMode["ENABLED"] = "enabled";
|
|
2107
|
+
KeyboardMode["DISABLED"] = "disabled";
|
|
2108
|
+
})(KeyboardMode || (KeyboardMode = {}));
|
|
2109
|
+
var ModalIntent;
|
|
2110
|
+
(function (ModalIntent) {
|
|
2111
|
+
ModalIntent["NEUTRAL"] = "neutral";
|
|
2112
|
+
ModalIntent["INFO"] = "info";
|
|
2113
|
+
ModalIntent["SUCCESS"] = "success";
|
|
2114
|
+
ModalIntent["WARNING"] = "warning";
|
|
2115
|
+
ModalIntent["DANGER"] = "danger";
|
|
2116
|
+
})(ModalIntent || (ModalIntent = {}));
|
|
2117
|
+
var WizardFlowMode;
|
|
2118
|
+
(function (WizardFlowMode) {
|
|
2119
|
+
WizardFlowMode["LINEAR"] = "linear";
|
|
2120
|
+
WizardFlowMode["FREE"] = "free";
|
|
2121
|
+
})(WizardFlowMode || (WizardFlowMode = {}));
|
|
2122
|
+
var FormLayoutMode;
|
|
2123
|
+
(function (FormLayoutMode) {
|
|
2124
|
+
FormLayoutMode["SINGLE_COLUMN"] = "single-column";
|
|
2125
|
+
FormLayoutMode["TWO_COLUMN"] = "two-column";
|
|
2126
|
+
FormLayoutMode["INLINE"] = "inline";
|
|
2127
|
+
})(FormLayoutMode || (FormLayoutMode = {}));
|
|
2128
|
+
var SubmitMode;
|
|
2129
|
+
(function (SubmitMode) {
|
|
2130
|
+
SubmitMode["ONCE"] = "once";
|
|
2131
|
+
SubmitMode["RETRYABLE"] = "retryable";
|
|
2132
|
+
})(SubmitMode || (SubmitMode = {}));
|
|
2133
|
+
var ConfirmationTone;
|
|
2134
|
+
(function (ConfirmationTone) {
|
|
2135
|
+
ConfirmationTone["DEFAULT"] = "default";
|
|
2136
|
+
ConfirmationTone["WARNING"] = "warning";
|
|
2137
|
+
ConfirmationTone["DANGER"] = "danger";
|
|
2138
|
+
})(ConfirmationTone || (ConfirmationTone = {}));
|
|
2139
|
+
var FieldKind;
|
|
2140
|
+
(function (FieldKind) {
|
|
2141
|
+
FieldKind["TEXT"] = "text";
|
|
2142
|
+
FieldKind["NUMBER"] = "number";
|
|
2143
|
+
FieldKind["SELECT"] = "select";
|
|
2144
|
+
FieldKind["CHECKBOX"] = "checkbox";
|
|
2145
|
+
FieldKind["DATE"] = "date";
|
|
2146
|
+
FieldKind["TEXTAREA"] = "textarea";
|
|
2147
|
+
FieldKind["DATETIME"] = "datetime";
|
|
2148
|
+
FieldKind["MULTI_SELECT"] = "multi-select";
|
|
2149
|
+
FieldKind["MULTI_SELECT_TABLE"] = "multi-select-table";
|
|
2150
|
+
FieldKind["SINGLE_SELECT_TABLE"] = "single-select-table";
|
|
2151
|
+
FieldKind["PASSWORD"] = "password";
|
|
2152
|
+
FieldKind["FILE"] = "file";
|
|
2153
|
+
FieldKind["COLOR"] = "color";
|
|
2154
|
+
FieldKind["RATING"] = "rating";
|
|
2155
|
+
FieldKind["SLIDER"] = "slider";
|
|
2156
|
+
FieldKind["CUSTOM"] = "custom";
|
|
2157
|
+
})(FieldKind || (FieldKind = {}));
|
|
2158
|
+
var FieldAppearance;
|
|
2159
|
+
(function (FieldAppearance) {
|
|
2160
|
+
FieldAppearance["OUTLINE"] = "outline";
|
|
2161
|
+
FieldAppearance["FILLED"] = "filled";
|
|
2162
|
+
FieldAppearance["GHOST"] = "ghost";
|
|
2163
|
+
})(FieldAppearance || (FieldAppearance = {}));
|
|
2164
|
+
var SelectionMode;
|
|
2165
|
+
(function (SelectionMode) {
|
|
2166
|
+
SelectionMode["SINGLE"] = "single";
|
|
2167
|
+
SelectionMode["MULTIPLE"] = "multiple";
|
|
2168
|
+
})(SelectionMode || (SelectionMode = {}));
|
|
2169
|
+
var OptionState;
|
|
2170
|
+
(function (OptionState) {
|
|
2171
|
+
OptionState["ENABLED"] = "enabled";
|
|
2172
|
+
OptionState["DISABLED"] = "disabled";
|
|
2173
|
+
})(OptionState || (OptionState = {}));
|
|
2174
|
+
var ActionStyle;
|
|
2175
|
+
(function (ActionStyle) {
|
|
2176
|
+
ActionStyle["PRIMARY"] = "primary";
|
|
2177
|
+
ActionStyle["SECONDARY"] = "secondary";
|
|
2178
|
+
ActionStyle["DANGER"] = "danger";
|
|
2179
|
+
ActionStyle["GHOST"] = "ghost";
|
|
2180
|
+
})(ActionStyle || (ActionStyle = {}));
|
|
2181
|
+
var StepState;
|
|
2182
|
+
(function (StepState) {
|
|
2183
|
+
StepState["PENDING"] = "pending";
|
|
2184
|
+
StepState["ACTIVE"] = "active";
|
|
2185
|
+
StepState["COMPLETE"] = "complete";
|
|
2186
|
+
StepState["DISABLED"] = "disabled";
|
|
2187
|
+
StepState["HIDDEN"] = "hidden";
|
|
2188
|
+
})(StepState || (StepState = {}));
|
|
2189
|
+
var NavigationDirection;
|
|
2190
|
+
(function (NavigationDirection) {
|
|
2191
|
+
NavigationDirection["FORWARD"] = "forward";
|
|
2192
|
+
NavigationDirection["BACKWARD"] = "backward";
|
|
2193
|
+
NavigationDirection["DIRECT"] = "direct";
|
|
2194
|
+
})(NavigationDirection || (NavigationDirection = {}));
|
|
2195
|
+
var ModalCloseReason;
|
|
2196
|
+
(function (ModalCloseReason) {
|
|
2197
|
+
ModalCloseReason["COMPLETED"] = "completed";
|
|
2198
|
+
ModalCloseReason["CANCELLED"] = "cancelled";
|
|
2199
|
+
ModalCloseReason["DISMISSED"] = "dismissed";
|
|
2200
|
+
ModalCloseReason["BACKDROP"] = "backdrop";
|
|
2201
|
+
ModalCloseReason["ESCAPE"] = "escape";
|
|
2202
|
+
ModalCloseReason["PROGRAMMATIC"] = "programmatic";
|
|
2203
|
+
ModalCloseReason["GUARD_REJECTED"] = "guard_rejected";
|
|
2204
|
+
})(ModalCloseReason || (ModalCloseReason = {}));
|
|
2205
|
+
var ValidationStatus;
|
|
2206
|
+
(function (ValidationStatus) {
|
|
2207
|
+
ValidationStatus["VALID"] = "valid";
|
|
2208
|
+
ValidationStatus["INVALID"] = "invalid";
|
|
2209
|
+
ValidationStatus["PENDING"] = "pending";
|
|
2210
|
+
})(ValidationStatus || (ValidationStatus = {}));
|
|
2211
|
+
var ValidationCode;
|
|
2212
|
+
(function (ValidationCode) {
|
|
2213
|
+
ValidationCode["REQUIRED"] = "required";
|
|
2214
|
+
ValidationCode["MIN"] = "min";
|
|
2215
|
+
ValidationCode["MAX"] = "max";
|
|
2216
|
+
ValidationCode["PATTERN"] = "pattern";
|
|
2217
|
+
ValidationCode["CUSTOM"] = "custom";
|
|
2218
|
+
})(ValidationCode || (ValidationCode = {}));
|
|
2219
|
+
|
|
2220
|
+
class StepBuilder {
|
|
2221
|
+
config;
|
|
2222
|
+
currentRow = [];
|
|
2223
|
+
currentRowColumns = 1;
|
|
2224
|
+
constructor(id, title) {
|
|
2225
|
+
this.config = {
|
|
2226
|
+
id,
|
|
2227
|
+
title,
|
|
2228
|
+
fields: [],
|
|
2229
|
+
state: StepState.PENDING,
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
body(body) {
|
|
2233
|
+
this.config.body = body;
|
|
2234
|
+
return this;
|
|
2235
|
+
}
|
|
2236
|
+
state(state) {
|
|
2237
|
+
this.config.state = state;
|
|
2238
|
+
return this;
|
|
2239
|
+
}
|
|
2240
|
+
guard(guard) {
|
|
2241
|
+
this.config.guard = guard;
|
|
2242
|
+
return this;
|
|
2243
|
+
}
|
|
2244
|
+
validators(validators) {
|
|
2245
|
+
this.config.validators = validators;
|
|
2246
|
+
return this;
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Add a field to this step. This is the single API for all field types.
|
|
2250
|
+
*
|
|
2251
|
+
* @example
|
|
2252
|
+
* s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
|
|
2253
|
+
* s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
|
|
2254
|
+
*/
|
|
2255
|
+
field(field) {
|
|
2256
|
+
this.flushCurrentRow();
|
|
2257
|
+
this.config.fields = this.config.fields || [];
|
|
2258
|
+
this.config.fields.push(field);
|
|
2259
|
+
if (!this.config.rows) {
|
|
2260
|
+
this.config.rows = [];
|
|
2261
|
+
}
|
|
2262
|
+
this.config.rows.push({
|
|
2263
|
+
columns: 1,
|
|
2264
|
+
fields: [{ field, span: 1 }],
|
|
2265
|
+
});
|
|
2266
|
+
return this;
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Start a new row with the specified number of columns.
|
|
2270
|
+
* All subsequent `addToRow()` calls will add fields to this row.
|
|
2271
|
+
*/
|
|
2272
|
+
row(columns = 2) {
|
|
2273
|
+
this.flushCurrentRow();
|
|
2274
|
+
this.currentRowColumns = columns;
|
|
2275
|
+
return this;
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Add a field to the current row started by `row()`.
|
|
2279
|
+
*/
|
|
2280
|
+
addToRow(field, span = 1) {
|
|
2281
|
+
this.config.fields = this.config.fields || [];
|
|
2282
|
+
this.config.fields.push(field);
|
|
2283
|
+
this.currentRow.push({ field, span });
|
|
2284
|
+
return this;
|
|
2285
|
+
}
|
|
2286
|
+
flushCurrentRow() {
|
|
2287
|
+
if (this.currentRow.length > 0) {
|
|
2288
|
+
if (!this.config.rows) {
|
|
2289
|
+
this.config.rows = [];
|
|
2290
|
+
}
|
|
2291
|
+
this.config.rows.push({
|
|
2292
|
+
columns: this.currentRowColumns,
|
|
2293
|
+
fields: [...this.currentRow],
|
|
2294
|
+
});
|
|
2295
|
+
this.currentRow = [];
|
|
2296
|
+
this.currentRowColumns = 1;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Add a field group with a section header.
|
|
2301
|
+
*/
|
|
2302
|
+
fieldGroup(group) {
|
|
2303
|
+
this.flushCurrentRow();
|
|
2304
|
+
if (!this.config.fieldGroups) {
|
|
2305
|
+
this.config.fieldGroups = [];
|
|
2306
|
+
}
|
|
2307
|
+
this.config.fields = this.config.fields || [];
|
|
2308
|
+
group.fields.forEach(f => this.config.fields.push(f));
|
|
2309
|
+
if (!group.rows) {
|
|
2310
|
+
group.rows = group.fields.map(f => ({
|
|
2311
|
+
columns: 1,
|
|
2312
|
+
fields: [{ field: f, span: 1 }],
|
|
2313
|
+
}));
|
|
2314
|
+
}
|
|
2315
|
+
this.config.fieldGroups.push(group);
|
|
2316
|
+
return this;
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Add form-level validators for cross-field validation within this step.
|
|
2320
|
+
*/
|
|
2321
|
+
formValidators(validators) {
|
|
2322
|
+
this.config.formValidators = validators;
|
|
2323
|
+
return this;
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Add Angular FormGroup-level validators for this step.
|
|
2327
|
+
*/
|
|
2328
|
+
groupValidators(validators) {
|
|
2329
|
+
this.config.groupValidators = validators;
|
|
2330
|
+
return this;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Set initial values for fields in this step.
|
|
2334
|
+
*/
|
|
2335
|
+
initialValue(value) {
|
|
2336
|
+
this.config.initialValue = value;
|
|
2337
|
+
return this;
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Set a visibility condition for this step based on aggregated wizard data.
|
|
2341
|
+
*/
|
|
2342
|
+
visible(condition) {
|
|
2343
|
+
this.config.visible = condition;
|
|
2344
|
+
return this;
|
|
2345
|
+
}
|
|
2346
|
+
build() {
|
|
2347
|
+
this.flushCurrentRow();
|
|
2348
|
+
return this.config;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
class BaseModalBuilder {
|
|
2353
|
+
config;
|
|
2354
|
+
constructor(initialConfig) {
|
|
2355
|
+
this.config = initialConfig;
|
|
2356
|
+
}
|
|
2357
|
+
title(title) {
|
|
2358
|
+
this.config.title = title;
|
|
2359
|
+
return this;
|
|
2360
|
+
}
|
|
2361
|
+
subtitle(subtitle) {
|
|
2362
|
+
this.config.subtitle = subtitle;
|
|
2363
|
+
return this;
|
|
2364
|
+
}
|
|
2365
|
+
description(description) {
|
|
2366
|
+
this.config.description = description;
|
|
2367
|
+
return this;
|
|
2368
|
+
}
|
|
2369
|
+
closeGuard(guard) {
|
|
2370
|
+
this.config.closeGuard = guard;
|
|
2371
|
+
return this;
|
|
2372
|
+
}
|
|
2373
|
+
size(size) {
|
|
2374
|
+
this.config.size = size;
|
|
2375
|
+
return this;
|
|
2376
|
+
}
|
|
2377
|
+
closeMode(mode) {
|
|
2378
|
+
this.config.closeMode = mode;
|
|
2379
|
+
return this;
|
|
2380
|
+
}
|
|
2381
|
+
backdrop(mode) {
|
|
2382
|
+
this.config.backdrop = mode;
|
|
2383
|
+
return this;
|
|
2384
|
+
}
|
|
2385
|
+
keyboard(mode) {
|
|
2386
|
+
this.config.keyboard = mode;
|
|
2387
|
+
return this;
|
|
2388
|
+
}
|
|
2389
|
+
intent(intent) {
|
|
2390
|
+
this.config.intent = intent;
|
|
2391
|
+
return this;
|
|
2392
|
+
}
|
|
2393
|
+
footerActions(actions) {
|
|
2394
|
+
this.config.footerActions = actions;
|
|
2395
|
+
return this;
|
|
2396
|
+
}
|
|
2397
|
+
polling(config) {
|
|
2398
|
+
this.config.polling = config;
|
|
2399
|
+
return this;
|
|
2400
|
+
}
|
|
2401
|
+
onCancel(handler) {
|
|
2402
|
+
this.config.onCancel = handler;
|
|
2403
|
+
return this;
|
|
2404
|
+
}
|
|
2405
|
+
i18n(labels) {
|
|
2406
|
+
this.config.i18n = labels;
|
|
2407
|
+
return this;
|
|
2408
|
+
}
|
|
2409
|
+
build() {
|
|
2410
|
+
return Object.freeze({ ...this.config });
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
class WizardModalBuilder extends BaseModalBuilder {
|
|
2415
|
+
constructor() {
|
|
2416
|
+
super({
|
|
2417
|
+
kind: ModalKind.WIZARD,
|
|
2418
|
+
steps: [],
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
step(step) {
|
|
2422
|
+
if (typeof step === 'function') {
|
|
2423
|
+
const builder = new StepBuilder('', '');
|
|
2424
|
+
step(builder);
|
|
2425
|
+
const builtStep = builder.build();
|
|
2426
|
+
this.config.steps.push(builtStep);
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
this.config.steps.push(step);
|
|
2430
|
+
}
|
|
2431
|
+
return this;
|
|
2432
|
+
}
|
|
2433
|
+
addStep(title, buildFn, id) {
|
|
2434
|
+
const stepId = id || `step-${this.config.steps.length}`;
|
|
2435
|
+
const builder = new StepBuilder(stepId, title);
|
|
2436
|
+
buildFn(builder);
|
|
2437
|
+
this.config.steps.push(builder.build());
|
|
2438
|
+
return this;
|
|
2439
|
+
}
|
|
2440
|
+
startAt(stepId) {
|
|
2441
|
+
this.config.startStepId = stepId;
|
|
2442
|
+
return this;
|
|
2443
|
+
}
|
|
2444
|
+
flow(mode) {
|
|
2445
|
+
this.config.flow = mode;
|
|
2446
|
+
return this;
|
|
2447
|
+
}
|
|
2448
|
+
onStepChange(handler) {
|
|
2449
|
+
this.config.onStepChange = handler;
|
|
2450
|
+
return this;
|
|
2451
|
+
}
|
|
2452
|
+
onComplete(handler) {
|
|
2453
|
+
this.config.onComplete = handler;
|
|
2454
|
+
return this;
|
|
2455
|
+
}
|
|
2456
|
+
onBeforeComplete(validators) {
|
|
2457
|
+
this.config.onBeforeComplete = validators;
|
|
2458
|
+
return this;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
class FormModalBuilder extends BaseModalBuilder {
|
|
2463
|
+
currentRow = [];
|
|
2464
|
+
currentRowColumns = 1;
|
|
2465
|
+
constructor() {
|
|
2466
|
+
super({
|
|
2467
|
+
kind: ModalKind.FORM,
|
|
2468
|
+
fields: [],
|
|
2469
|
+
rows: [],
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Add a field as a full-width row (single column).
|
|
2474
|
+
* This is the simple API — each field gets its own row.
|
|
2475
|
+
*/
|
|
2476
|
+
field(field) {
|
|
2477
|
+
this.flushCurrentRow();
|
|
2478
|
+
this.config.fields.push(field);
|
|
2479
|
+
this.config.rows.push({
|
|
2480
|
+
columns: 1,
|
|
2481
|
+
fields: [{ field, span: 1 }],
|
|
2482
|
+
});
|
|
2483
|
+
return this;
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Start a new row with the specified number of columns.
|
|
2487
|
+
* All subsequent `addToRow()` calls will add fields to this row
|
|
2488
|
+
* until the next `row()` or `field()` call.
|
|
2489
|
+
*
|
|
2490
|
+
* @example
|
|
2491
|
+
* .row(2)
|
|
2492
|
+
* .addToRow({ kind: FieldKind.TEXT, key: 'firstName', label: 'First Name' })
|
|
2493
|
+
* .addToRow({ kind: FieldKind.TEXT, key: 'lastName', label: 'Last Name' })
|
|
2494
|
+
* .row(3)
|
|
2495
|
+
* .addToRow({ kind: FieldKind.TEXT, key: 'city', label: 'City' }, 2)
|
|
2496
|
+
* .addToRow({ kind: FieldKind.TEXT, key: 'zip', label: 'ZIP' })
|
|
2497
|
+
*/
|
|
2498
|
+
row(columns = 2) {
|
|
2499
|
+
this.flushCurrentRow();
|
|
2500
|
+
this.currentRowColumns = columns;
|
|
2501
|
+
return this;
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Add a field to the current row started by `row()`.
|
|
2505
|
+
* @param field - The field configuration
|
|
2506
|
+
* @param span - How many columns this field should span (default: 1)
|
|
2507
|
+
*/
|
|
2508
|
+
addToRow(field, span = 1) {
|
|
2509
|
+
this.config.fields.push(field);
|
|
2510
|
+
this.currentRow.push({ field, span });
|
|
2511
|
+
return this;
|
|
2512
|
+
}
|
|
2513
|
+
flushCurrentRow() {
|
|
2514
|
+
if (this.currentRow.length > 0) {
|
|
2515
|
+
this.config.rows.push({
|
|
2516
|
+
columns: this.currentRowColumns,
|
|
2517
|
+
fields: [...this.currentRow],
|
|
2518
|
+
});
|
|
2519
|
+
this.currentRow = [];
|
|
2520
|
+
this.currentRowColumns = 1;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
layout(mode) {
|
|
2524
|
+
this.config.layout = mode;
|
|
2525
|
+
return this;
|
|
2526
|
+
}
|
|
2527
|
+
initialValue(value) {
|
|
2528
|
+
this.config.initialValue = value;
|
|
2529
|
+
return this;
|
|
2530
|
+
}
|
|
2531
|
+
submitMode(mode) {
|
|
2532
|
+
this.config.submitMode = mode;
|
|
2533
|
+
return this;
|
|
2534
|
+
}
|
|
2535
|
+
onComplete(handler) {
|
|
2536
|
+
this.config.onComplete = handler;
|
|
2537
|
+
return this;
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Add form-level validators for cross-field validation.
|
|
2541
|
+
* These receive the entire form value and return an error map or null.
|
|
2542
|
+
*/
|
|
2543
|
+
formValidators(validators) {
|
|
2544
|
+
this.config.formValidators = validators;
|
|
2545
|
+
return this;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Add Angular FormGroup-level validators.
|
|
2549
|
+
* These are standard Angular ValidatorFn applied to the FormGroup itself.
|
|
2550
|
+
*/
|
|
2551
|
+
groupValidators(validators) {
|
|
2552
|
+
this.config.groupValidators = validators;
|
|
2553
|
+
return this;
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Add a field group with a section header.
|
|
2557
|
+
* Groups visually separate fields with a title and optional description.
|
|
2558
|
+
*/
|
|
2559
|
+
fieldGroup(group) {
|
|
2560
|
+
this.flushCurrentRow();
|
|
2561
|
+
if (!this.config.fieldGroups) {
|
|
2562
|
+
this.config.fieldGroups = [];
|
|
2563
|
+
}
|
|
2564
|
+
// Also add group fields to the flat fields array for form control creation
|
|
2565
|
+
group.fields.forEach(f => this.config.fields.push(f));
|
|
2566
|
+
// Build rows for the group if not provided
|
|
2567
|
+
if (!group.rows) {
|
|
2568
|
+
group.rows = group.fields.map(f => ({
|
|
2569
|
+
columns: 1,
|
|
2570
|
+
fields: [{ field: f, span: 1 }],
|
|
2571
|
+
}));
|
|
2572
|
+
}
|
|
2573
|
+
this.config.fieldGroups.push(group);
|
|
2574
|
+
return this;
|
|
2575
|
+
}
|
|
2576
|
+
build() {
|
|
2577
|
+
this.flushCurrentRow();
|
|
2578
|
+
return super.build();
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
class ConfirmationModalBuilder extends BaseModalBuilder {
|
|
2583
|
+
constructor() {
|
|
2584
|
+
super({
|
|
2585
|
+
kind: ModalKind.CONFIRMATION,
|
|
2586
|
+
message: '',
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
message(text) {
|
|
2590
|
+
this.config.message = text;
|
|
2591
|
+
return this;
|
|
2592
|
+
}
|
|
2593
|
+
tone(tone) {
|
|
2594
|
+
this.config.tone = tone;
|
|
2595
|
+
return this;
|
|
2596
|
+
}
|
|
2597
|
+
confirmAction(action) {
|
|
2598
|
+
this.config.confirm = action;
|
|
2599
|
+
return this;
|
|
2600
|
+
}
|
|
2601
|
+
cancelAction(action) {
|
|
2602
|
+
this.config.cancel = action;
|
|
2603
|
+
return this;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
class CustomModalBuilder extends BaseModalBuilder {
|
|
2608
|
+
constructor() {
|
|
2609
|
+
super({
|
|
2610
|
+
kind: ModalKind.CUSTOM,
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
component(component) {
|
|
2614
|
+
this.config.component = component;
|
|
2615
|
+
return this;
|
|
2616
|
+
}
|
|
2617
|
+
template(template) {
|
|
2618
|
+
this.config.template = template;
|
|
2619
|
+
return this;
|
|
2620
|
+
}
|
|
2621
|
+
inputs(inputs) {
|
|
2622
|
+
this.config.inputs = inputs;
|
|
2623
|
+
return this;
|
|
2624
|
+
}
|
|
2625
|
+
onComplete(handler) {
|
|
2626
|
+
this.config.onComplete = handler;
|
|
2627
|
+
return this;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
class ModalBuilder {
|
|
2632
|
+
static wizard() {
|
|
2633
|
+
return new WizardModalBuilder();
|
|
2634
|
+
}
|
|
2635
|
+
static form() {
|
|
2636
|
+
return new FormModalBuilder();
|
|
2637
|
+
}
|
|
2638
|
+
static confirmation() {
|
|
2639
|
+
return new ConfirmationModalBuilder();
|
|
2640
|
+
}
|
|
2641
|
+
static custom() {
|
|
2642
|
+
return new CustomModalBuilder();
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
class MnModalRef {
|
|
2647
|
+
componentRef;
|
|
2648
|
+
config;
|
|
2649
|
+
closeSubject = new Subject();
|
|
2650
|
+
afterClosed$ = this.closeSubject.asObservable();
|
|
2651
|
+
constructor(componentRef, config) {
|
|
2652
|
+
this.componentRef = componentRef;
|
|
2653
|
+
this.config = config;
|
|
2654
|
+
}
|
|
2655
|
+
close(result) {
|
|
2656
|
+
const event = {
|
|
2657
|
+
reason: ModalCloseReason.COMPLETED,
|
|
2658
|
+
result,
|
|
2659
|
+
};
|
|
2660
|
+
this.animateAndDestroy(event);
|
|
2661
|
+
}
|
|
2662
|
+
dismiss(reason) {
|
|
2663
|
+
const event = {
|
|
2664
|
+
reason,
|
|
2665
|
+
};
|
|
2666
|
+
this.animateAndDestroy(event);
|
|
2667
|
+
}
|
|
2668
|
+
async animateAndDestroy(event) {
|
|
2669
|
+
const shell = this.componentRef.instance;
|
|
2670
|
+
if (shell && typeof shell.startClosing === 'function') {
|
|
2671
|
+
await shell.startClosing();
|
|
2672
|
+
}
|
|
2673
|
+
this.closeSubject.next(event);
|
|
2674
|
+
this.closeSubject.complete();
|
|
2675
|
+
this.destroy();
|
|
2676
|
+
}
|
|
2677
|
+
update(config) {
|
|
2678
|
+
Object.assign(this.config, config);
|
|
2679
|
+
// Trigger change detection on the shell component
|
|
2680
|
+
this.componentRef.changeDetectorRef.detectChanges();
|
|
2681
|
+
}
|
|
2682
|
+
destroy() {
|
|
2683
|
+
this.componentRef.destroy();
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
class MnCustomFieldHostDirective {
|
|
2688
|
+
vcr;
|
|
2689
|
+
component;
|
|
2690
|
+
inputs;
|
|
2691
|
+
componentRef;
|
|
2692
|
+
onChange = () => { };
|
|
2693
|
+
onTouched = () => { };
|
|
2694
|
+
constructor(vcr) {
|
|
2695
|
+
this.vcr = vcr;
|
|
2696
|
+
}
|
|
2697
|
+
ngOnInit() {
|
|
2698
|
+
if (!this.component)
|
|
2699
|
+
return;
|
|
2700
|
+
this.vcr.clear();
|
|
2701
|
+
this.componentRef = this.vcr.createComponent(this.component);
|
|
2702
|
+
// Pass inputs to the component
|
|
2703
|
+
if (this.inputs) {
|
|
2704
|
+
Object.entries(this.inputs).forEach(([key, value]) => {
|
|
2705
|
+
this.componentRef.instance[key] = value;
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
// Wire up value accessor if the component supports it
|
|
2709
|
+
const instance = this.componentRef.instance;
|
|
2710
|
+
if (typeof instance.registerOnChange === 'function') {
|
|
2711
|
+
instance.registerOnChange((val) => this.onChange(val));
|
|
2712
|
+
}
|
|
2713
|
+
if (typeof instance.registerOnTouched === 'function') {
|
|
2714
|
+
instance.registerOnTouched(() => this.onTouched());
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
ngOnDestroy() {
|
|
2718
|
+
this.componentRef?.destroy();
|
|
2719
|
+
}
|
|
2720
|
+
writeValue(val) {
|
|
2721
|
+
const instance = this.componentRef?.instance;
|
|
2722
|
+
if (instance && typeof instance.writeValue === 'function') {
|
|
2723
|
+
instance.writeValue(val);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
registerOnChange(fn) {
|
|
2727
|
+
this.onChange = fn;
|
|
2728
|
+
}
|
|
2729
|
+
registerOnTouched(fn) {
|
|
2730
|
+
this.onTouched = fn;
|
|
2731
|
+
}
|
|
2732
|
+
setDisabledState(isDisabled) {
|
|
2733
|
+
const instance = this.componentRef?.instance;
|
|
2734
|
+
if (instance && typeof instance.setDisabledState === 'function') {
|
|
2735
|
+
instance.setDisabledState(isDisabled);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomFieldHostDirective, deps: [{ token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
2739
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnCustomFieldHostDirective, isStandalone: true, selector: "[mnCustomFieldHost]", inputs: { component: "component", inputs: "inputs" }, providers: [
|
|
2740
|
+
{
|
|
2741
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2742
|
+
useExisting: forwardRef(() => MnCustomFieldHostDirective),
|
|
2743
|
+
multi: true,
|
|
2744
|
+
},
|
|
2745
|
+
], ngImport: i0 });
|
|
2746
|
+
}
|
|
2747
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomFieldHostDirective, decorators: [{
|
|
2748
|
+
type: Directive,
|
|
2749
|
+
args: [{
|
|
2750
|
+
selector: '[mnCustomFieldHost]',
|
|
2751
|
+
standalone: true,
|
|
2752
|
+
providers: [
|
|
2753
|
+
{
|
|
2754
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2755
|
+
useExisting: forwardRef(() => MnCustomFieldHostDirective),
|
|
2756
|
+
multi: true,
|
|
2757
|
+
},
|
|
2758
|
+
],
|
|
2759
|
+
}]
|
|
2760
|
+
}], ctorParameters: () => [{ type: i0.ViewContainerRef }], propDecorators: { component: [{
|
|
2761
|
+
type: Input
|
|
2762
|
+
}], inputs: [{
|
|
2763
|
+
type: Input
|
|
2764
|
+
}] } });
|
|
2765
|
+
|
|
2766
|
+
// ── Column Sort Type ──
|
|
2767
|
+
var ColumnSortType;
|
|
2768
|
+
(function (ColumnSortType) {
|
|
2769
|
+
ColumnSortType["ALPHABETICAL"] = "ALPHABETICAL";
|
|
2770
|
+
ColumnSortType["NUMERICAL"] = "NUMERICAL";
|
|
2771
|
+
ColumnSortType["DATE"] = "DATE";
|
|
2772
|
+
ColumnSortType["NONE"] = "NONE";
|
|
2773
|
+
})(ColumnSortType || (ColumnSortType = {}));
|
|
2774
|
+
|
|
2775
|
+
class MnTable {
|
|
2776
|
+
dataSource;
|
|
2777
|
+
sortChange = new EventEmitter();
|
|
2778
|
+
selectionChange = new EventEmitter();
|
|
2779
|
+
rowClick = new EventEmitter();
|
|
2780
|
+
filteredItems = [];
|
|
2781
|
+
searchValue = '';
|
|
2782
|
+
loadingMoreRows = false;
|
|
2783
|
+
currentSort = null;
|
|
2784
|
+
selectedIds = new Set();
|
|
2785
|
+
dataSubscription;
|
|
2786
|
+
searchSubject = new Subject();
|
|
2787
|
+
searchSubscription;
|
|
2788
|
+
ngOnInit() {
|
|
2789
|
+
this.currentSort = this.dataSource.defaultSort ?? null;
|
|
2790
|
+
this.filteredItems = this.dataSource.dataRows.value;
|
|
2791
|
+
this.dataSubscription = this.dataSource.dataRows.subscribe(() => {
|
|
2792
|
+
this.applyFilterAndSort(false);
|
|
2793
|
+
});
|
|
2794
|
+
this.searchSubscription = this.searchSubject
|
|
2795
|
+
.pipe(debounceTime(300))
|
|
2796
|
+
.subscribe(value => {
|
|
2797
|
+
this.searchValue = value;
|
|
2798
|
+
this.applyFilterAndSort(true);
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
ngOnDestroy() {
|
|
2802
|
+
this.dataSubscription?.unsubscribe();
|
|
2803
|
+
this.searchSubscription?.unsubscribe();
|
|
2804
|
+
}
|
|
2805
|
+
// ── Search ──
|
|
2806
|
+
onSearch(searchString) {
|
|
2807
|
+
this.searchSubject.next(searchString);
|
|
2808
|
+
}
|
|
2809
|
+
// ── Sorting ──
|
|
2810
|
+
sort(column) {
|
|
2811
|
+
if (!column.sortType || column.sortType === ColumnSortType.NONE)
|
|
2812
|
+
return;
|
|
2813
|
+
if (this.currentSort?.columnKey === column.key) {
|
|
2814
|
+
this.currentSort = this.currentSort.direction === 'asc'
|
|
2815
|
+
? { columnKey: column.key, direction: 'desc' }
|
|
2816
|
+
: null;
|
|
2817
|
+
}
|
|
2818
|
+
else {
|
|
2819
|
+
this.currentSort = { columnKey: column.key, direction: 'asc' };
|
|
2820
|
+
}
|
|
2821
|
+
this.sortChange.emit(this.currentSort);
|
|
2822
|
+
this.applyFilterAndSort(false);
|
|
2823
|
+
}
|
|
2824
|
+
getSortIcon(column) {
|
|
2825
|
+
if (!this.currentSort || this.currentSort.columnKey !== column.key)
|
|
2826
|
+
return '';
|
|
2827
|
+
return this.currentSort.direction === 'asc' ? '▲' : '▼';
|
|
2828
|
+
}
|
|
2829
|
+
isSortable(column) {
|
|
2830
|
+
return !!column.sortType && column.sortType !== ColumnSortType.NONE;
|
|
2831
|
+
}
|
|
2832
|
+
// ── Selection ──
|
|
2833
|
+
isSelected(row) {
|
|
2834
|
+
return this.selectedIds.has(this.dataSource.getID(row));
|
|
2835
|
+
}
|
|
2836
|
+
toggleRow(row) {
|
|
2837
|
+
const id = this.dataSource.getID(row);
|
|
2838
|
+
const mode = this.dataSource.selectionMode ?? 'none';
|
|
2839
|
+
if (mode === 'single') {
|
|
2840
|
+
this.selectedIds.clear();
|
|
2841
|
+
this.selectedIds.add(id);
|
|
2842
|
+
}
|
|
2843
|
+
else if (mode === 'multi') {
|
|
2844
|
+
if (this.selectedIds.has(id)) {
|
|
2845
|
+
this.selectedIds.delete(id);
|
|
2846
|
+
}
|
|
2847
|
+
else {
|
|
2848
|
+
this.selectedIds.add(id);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
this.emitSelection();
|
|
2852
|
+
}
|
|
2853
|
+
toggleAll() {
|
|
2854
|
+
if (this.selectedIds.size === this.filteredItems.length) {
|
|
2855
|
+
this.selectedIds.clear();
|
|
2856
|
+
}
|
|
2857
|
+
else {
|
|
2858
|
+
this.filteredItems.forEach(row => this.selectedIds.add(this.dataSource.getID(row)));
|
|
2859
|
+
}
|
|
2860
|
+
this.emitSelection();
|
|
2861
|
+
}
|
|
2862
|
+
get allSelected() {
|
|
2863
|
+
return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
|
|
2864
|
+
}
|
|
2865
|
+
get hasSelection() {
|
|
2866
|
+
return (this.dataSource.selectionMode ?? 'none') !== 'none';
|
|
2867
|
+
}
|
|
2868
|
+
get isMultiSelect() {
|
|
2869
|
+
return this.dataSource.selectionMode === 'multi';
|
|
2870
|
+
}
|
|
2871
|
+
// ── Row interaction ──
|
|
2872
|
+
onRowClick(row) {
|
|
2873
|
+
this.dataSource.onRowClick?.(row);
|
|
2874
|
+
this.rowClick.emit(row);
|
|
2875
|
+
}
|
|
2876
|
+
// ── Pagination ──
|
|
2877
|
+
loadMoreRows() {
|
|
2878
|
+
if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
|
|
2879
|
+
return;
|
|
2880
|
+
this.loadingMoreRows = true;
|
|
2881
|
+
const promise = (this.searchValue.length > 0 && this.dataSource.searchForAdditionalItems)
|
|
2882
|
+
? this.dataSource.searchForAdditionalItems(this.searchValue)
|
|
2883
|
+
: this.dataSource.loadAdditionalRows();
|
|
2884
|
+
promise
|
|
2885
|
+
.then(rows => this.processLoadedRows(rows))
|
|
2886
|
+
.catch(() => this.loadingMoreRows = false);
|
|
2887
|
+
}
|
|
2888
|
+
get showLoadMore() {
|
|
2889
|
+
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
2890
|
+
const strategy = this.dataSource.paginationStrategy;
|
|
2891
|
+
const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
|
|
2892
|
+
return mode === 'load-more' && hasMore;
|
|
2893
|
+
}
|
|
2894
|
+
// ── Template helpers ──
|
|
2895
|
+
isTemplateRef(value) {
|
|
2896
|
+
return value instanceof TemplateRef;
|
|
2897
|
+
}
|
|
2898
|
+
getCellValue(column, row) {
|
|
2899
|
+
if (typeof column.cell === 'function')
|
|
2900
|
+
return column.cell(row);
|
|
2901
|
+
return '';
|
|
2902
|
+
}
|
|
2903
|
+
trackByID = (_index, row) => {
|
|
2904
|
+
return this.dataSource.getID(row);
|
|
2905
|
+
};
|
|
2906
|
+
trackByKey = (_index, column) => {
|
|
2907
|
+
return column.key;
|
|
2908
|
+
};
|
|
2909
|
+
// ── Table CSS classes ──
|
|
2910
|
+
get tableClasses() {
|
|
2911
|
+
return 'w-full border-collapse overflow-y-hidden';
|
|
2912
|
+
}
|
|
2913
|
+
get hasRowActions() {
|
|
2914
|
+
return !!this.dataSource.rowActions && this.dataSource.rowActions.length > 0;
|
|
2915
|
+
}
|
|
2916
|
+
getVisibleActions(row) {
|
|
2917
|
+
return (this.dataSource.rowActions ?? []).filter(a => !a.isVisible || a.isVisible(row));
|
|
2918
|
+
}
|
|
2919
|
+
get totalColumnCount() {
|
|
2920
|
+
let count = this.dataSource.columns.length;
|
|
2921
|
+
if (this.hasSelection)
|
|
2922
|
+
count++;
|
|
2923
|
+
if (this.hasRowActions)
|
|
2924
|
+
count++;
|
|
2925
|
+
return count;
|
|
2926
|
+
}
|
|
2927
|
+
// ── Skeleton rows for loading ──
|
|
2928
|
+
get skeletonRows() {
|
|
2929
|
+
return Array.from({ length: 5 });
|
|
2930
|
+
}
|
|
2931
|
+
// ── Private ──
|
|
2932
|
+
applyFilterAndSort(searchForItems) {
|
|
2933
|
+
let items = this.dataSource.dataRows.value;
|
|
2934
|
+
if (this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
|
|
2935
|
+
const term = this.searchValue.toLowerCase();
|
|
2936
|
+
items = items.filter(row => this.dataSource.isInSearch(row, term));
|
|
2937
|
+
}
|
|
2938
|
+
items = this.applySorting(items);
|
|
2939
|
+
this.filteredItems = items;
|
|
2940
|
+
if (searchForItems) {
|
|
2941
|
+
this.loadMoreRows();
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
applySorting(items) {
|
|
2945
|
+
if (!this.currentSort)
|
|
2946
|
+
return items;
|
|
2947
|
+
const column = this.dataSource.columns.find(c => c.key === this.currentSort.columnKey);
|
|
2948
|
+
if (!column || !column.sortType || column.sortType === ColumnSortType.NONE)
|
|
2949
|
+
return items;
|
|
2950
|
+
const getValue = column.getRawValueToSort ?? ((row) => {
|
|
2951
|
+
if (typeof column.cell === 'function')
|
|
2952
|
+
return column.cell(row);
|
|
2953
|
+
return '';
|
|
2954
|
+
});
|
|
2955
|
+
const dir = this.currentSort.direction === 'asc' ? 1 : -1;
|
|
2956
|
+
return [...items].sort((a, b) => {
|
|
2957
|
+
const va = getValue(a);
|
|
2958
|
+
const vb = getValue(b);
|
|
2959
|
+
if (va == null && vb == null)
|
|
2960
|
+
return 0;
|
|
2961
|
+
if (va == null)
|
|
2962
|
+
return 1;
|
|
2963
|
+
if (vb == null)
|
|
2964
|
+
return -1;
|
|
2965
|
+
switch (column.sortType) {
|
|
2966
|
+
case ColumnSortType.ALPHABETICAL:
|
|
2967
|
+
return String(va).localeCompare(String(vb)) * dir;
|
|
2968
|
+
case ColumnSortType.NUMERICAL:
|
|
2969
|
+
return (Number(va) - Number(vb)) * dir;
|
|
2970
|
+
case ColumnSortType.DATE:
|
|
2971
|
+
return (new Date(va).getTime() - new Date(vb).getTime()) * dir;
|
|
2972
|
+
default:
|
|
2973
|
+
return 0;
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
processLoadedRows(rows) {
|
|
2978
|
+
const merged = [...new Map([...this.dataSource.dataRows.value, ...rows].map(item => [this.dataSource.getID(item), item])).values()];
|
|
2979
|
+
this.dataSource.dataRows.next(merged);
|
|
2980
|
+
this.loadingMoreRows = false;
|
|
2981
|
+
this.applyFilterAndSort(false);
|
|
2982
|
+
}
|
|
2983
|
+
emitSelection() {
|
|
2984
|
+
const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
2985
|
+
this.dataSource.selectedRows?.next(rows);
|
|
2986
|
+
this.selectionChange.emit(rows);
|
|
2987
|
+
}
|
|
2988
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2989
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", inputs: { dataSource: "dataSource" }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick" }, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n <div *ngIf=\"dataSource.canSearch\">\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-gray-300 bg-gray-100 px-3 py-1.5 text-sm focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search table\"\n />\n </div>\n <ng-container *ngIf=\"dataSource.toolbarTemplate\" [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr>\n <!-- Selection checkbox column header -->\n <th *ngIf=\"hasSelection\" class=\"w-10 text-center text-sm bg-gray-50 px-2 py-2\">\n <label *ngIf=\"isMultiSelect\">\n <input\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n </th>\n\n <!-- Data columns -->\n <th\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\n class=\"text-sm px-4 py-2\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-gray-100]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n <ng-container *ngIf=\"isTemplateRef(column.header); else stringHeader\">\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n </ng-container>\n <ng-template #stringHeader>\n <span>{{ column.header }}</span>\n </ng-template>\n <span *ngIf=\"isSortable(column)\" class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n </span>\n </th>\n\n <!-- Actions column header -->\n <th *ngIf=\"hasRowActions\" class=\"text-sm px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Loading state -->\n <ng-container *ngIf=\"dataSource.isDataLoading; else content\">\n <tr *ngFor=\"let _ of skeletonRows\" class=\"animate-pulse\">\n <td *ngIf=\"hasSelection\" class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-gray-200\"></div></td>\n <td *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\" class=\"px-4 py-3\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n >\n <div class=\"h-4 w-3/4 rounded bg-gray-200\"></div>\n </td>\n <td *ngIf=\"hasRowActions\" class=\"px-4 py-3\"><div class=\"h-4 w-16 rounded bg-gray-200\"></div></td>\n </tr>\n </ng-container>\n\n <!-- Content -->\n <ng-template #content>\n <!-- Empty state -->\n <tr *ngIf=\"filteredItems.length === 0\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n <ng-container *ngIf=\"dataSource.emptyTemplate; else defaultEmpty\">\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"flex flex-col items-center gap-2 text-gray-400\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data rows -->\n <tr\n *ngFor=\"let row of filteredItems; trackBy: trackByID; let odd = odd; let last = last\"\n class=\"bg-white transition-colors duration-150\"\n [class.bg-brand-50]=\"isSelected(row)\"\n [class.bg-gray-50]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-gray-100]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-gray-200]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n <td *ngIf=\"hasSelection\" class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n *ngIf=\"isMultiSelect; else radioTpl\"\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n <ng-template #radioTpl>\n <input\n type=\"radio\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n name=\"mn-table-single-select\"\n />\n </ng-template>\n </label>\n </td>\n\n <!-- Data cells -->\n <td\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\n class=\"text-xs px-4 py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n [style.width]=\"column.width ?? null\"\n >\n <ng-container *ngIf=\"isTemplateRef(column.cell); else stringCell\">\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n </ng-container>\n <ng-template #stringCell>{{ getCellValue(column, row) }}</ng-template>\n </td>\n\n <!-- Row actions -->\n <td *ngIf=\"hasRowActions\" class=\"text-right px-4 py-2\">\n <div class=\"inline-flex items-center justify-end gap-2\">\n <button\n *ngFor=\"let action of getVisibleActions(row)\"\n class=\"px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n [ngClass]=\"action.cssClass ?? ''\"\n [disabled]=\"action.isDisabled ? action.isDisabled(row) : false\"\n (click)=\"$event.stopPropagation(); action.onClick(row)\"\n [attr.aria-label]=\"action.label\"\n >\n <span *ngIf=\"action.icon\" [innerHTML]=\"action.icon\"></span>\n <span *ngIf=\"action.label\">{{ action.label }}</span>\n </button>\n </div>\n </td>\n </tr>\n </ng-template>\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n<div *ngIf=\"showLoadMore\" class=\"flex justify-center py-4\">\n <button\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n <span *ngIf=\"loadingMoreRows\" class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n Load more\n </button>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
2990
|
+
}
|
|
2991
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
2992
|
+
type: Component,
|
|
2993
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgForOf, NgIf, NgTemplateOutlet], template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n <div *ngIf=\"dataSource.canSearch\">\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-gray-300 bg-gray-100 px-3 py-1.5 text-sm focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search table\"\n />\n </div>\n <ng-container *ngIf=\"dataSource.toolbarTemplate\" [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr>\n <!-- Selection checkbox column header -->\n <th *ngIf=\"hasSelection\" class=\"w-10 text-center text-sm bg-gray-50 px-2 py-2\">\n <label *ngIf=\"isMultiSelect\">\n <input\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n </th>\n\n <!-- Data columns -->\n <th\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\n class=\"text-sm px-4 py-2\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-gray-100]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n [style.width]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n <ng-container *ngIf=\"isTemplateRef(column.header); else stringHeader\">\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n </ng-container>\n <ng-template #stringHeader>\n <span>{{ column.header }}</span>\n </ng-template>\n <span *ngIf=\"isSortable(column)\" class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n </span>\n </th>\n\n <!-- Actions column header -->\n <th *ngIf=\"hasRowActions\" class=\"text-sm px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Loading state -->\n <ng-container *ngIf=\"dataSource.isDataLoading; else content\">\n <tr *ngFor=\"let _ of skeletonRows\" class=\"animate-pulse\">\n <td *ngIf=\"hasSelection\" class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-gray-200\"></div></td>\n <td *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\" class=\"px-4 py-3\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n >\n <div class=\"h-4 w-3/4 rounded bg-gray-200\"></div>\n </td>\n <td *ngIf=\"hasRowActions\" class=\"px-4 py-3\"><div class=\"h-4 w-16 rounded bg-gray-200\"></div></td>\n </tr>\n </ng-container>\n\n <!-- Content -->\n <ng-template #content>\n <!-- Empty state -->\n <tr *ngIf=\"filteredItems.length === 0\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n <ng-container *ngIf=\"dataSource.emptyTemplate; else defaultEmpty\">\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n </ng-container>\n <ng-template #defaultEmpty>\n <div class=\"flex flex-col items-center gap-2 text-gray-400\">\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n </ng-template>\n </td>\n </tr>\n\n <!-- Data rows -->\n <tr\n *ngFor=\"let row of filteredItems; trackBy: trackByID; let odd = odd; let last = last\"\n class=\"bg-white transition-colors duration-150\"\n [class.bg-brand-50]=\"isSelected(row)\"\n [class.bg-gray-50]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-gray-100]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-gray-200]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n <td *ngIf=\"hasSelection\" class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n *ngIf=\"isMultiSelect; else radioTpl\"\n type=\"checkbox\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n <ng-template #radioTpl>\n <input\n type=\"radio\"\n class=\"accent-brand-500\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n name=\"mn-table-single-select\"\n />\n </ng-template>\n </label>\n </td>\n\n <!-- Data cells -->\n <td\n *ngFor=\"let column of dataSource.columns; trackBy: trackByKey\"\n class=\"text-xs px-4 py-2\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.hidden]=\"column.hiddenBelow === 'sm' || column.hiddenBelow === 'md' || column.hiddenBelow === 'lg'\"\n [class.sm:table-cell]=\"column.hiddenBelow === 'sm'\"\n [class.md:table-cell]=\"column.hiddenBelow === 'md'\"\n [class.lg:table-cell]=\"column.hiddenBelow === 'lg'\"\n [style.width]=\"column.width ?? null\"\n >\n <ng-container *ngIf=\"isTemplateRef(column.cell); else stringCell\">\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n </ng-container>\n <ng-template #stringCell>{{ getCellValue(column, row) }}</ng-template>\n </td>\n\n <!-- Row actions -->\n <td *ngIf=\"hasRowActions\" class=\"text-right px-4 py-2\">\n <div class=\"inline-flex items-center justify-end gap-2\">\n <button\n *ngFor=\"let action of getVisibleActions(row)\"\n class=\"px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n [ngClass]=\"action.cssClass ?? ''\"\n [disabled]=\"action.isDisabled ? action.isDisabled(row) : false\"\n (click)=\"$event.stopPropagation(); action.onClick(row)\"\n [attr.aria-label]=\"action.label\"\n >\n <span *ngIf=\"action.icon\" [innerHTML]=\"action.icon\"></span>\n <span *ngIf=\"action.label\">{{ action.label }}</span>\n </button>\n </div>\n </td>\n </tr>\n </ng-template>\n </tbody>\n </table>\n</div>\n\n<!-- Load more button -->\n<div *ngIf=\"showLoadMore\" class=\"flex justify-center py-4\">\n <button\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n <span *ngIf=\"loadingMoreRows\" class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n Load more\n </button>\n</div>\n" }]
|
|
2994
|
+
}], propDecorators: { dataSource: [{
|
|
2995
|
+
type: Input
|
|
2996
|
+
}], sortChange: [{
|
|
2997
|
+
type: Output
|
|
2998
|
+
}], selectionChange: [{
|
|
2999
|
+
type: Output
|
|
3000
|
+
}], rowClick: [{
|
|
3001
|
+
type: Output
|
|
3002
|
+
}] } });
|
|
3003
|
+
|
|
3004
|
+
class MnFormBodyComponent {
|
|
3005
|
+
fb;
|
|
3006
|
+
config;
|
|
3007
|
+
modalRef;
|
|
3008
|
+
hideFooter = false;
|
|
3009
|
+
form;
|
|
3010
|
+
rows = [];
|
|
3011
|
+
fieldGroups = [];
|
|
3012
|
+
isSubmitting = false;
|
|
3013
|
+
FieldKind = FieldKind;
|
|
3014
|
+
ModalCloseReason = ModalCloseReason;
|
|
3015
|
+
/** Cross-field validation errors: { fieldKey: errorMessage } */
|
|
3016
|
+
formErrors = {};
|
|
3017
|
+
/** Track which fields are currently visible (for conditional fields) */
|
|
3018
|
+
fieldVisibility = {};
|
|
3019
|
+
/** Track loading state per field for async data sources */
|
|
3020
|
+
fieldLoading = {};
|
|
3021
|
+
/** Dynamic options loaded from data sources */
|
|
3022
|
+
fieldOptions = {};
|
|
3023
|
+
valueChangesSubscription;
|
|
3024
|
+
constructor(fb) {
|
|
3025
|
+
this.fb = fb;
|
|
3026
|
+
}
|
|
3027
|
+
asField(field) {
|
|
3028
|
+
return field;
|
|
3029
|
+
}
|
|
3030
|
+
asKey(key) {
|
|
3031
|
+
return key;
|
|
3032
|
+
}
|
|
3033
|
+
asAny(val) {
|
|
3034
|
+
return val;
|
|
3035
|
+
}
|
|
3036
|
+
hasRequiredValidator(field) {
|
|
3037
|
+
const validators = field.validators;
|
|
3038
|
+
if (!validators)
|
|
3039
|
+
return false;
|
|
3040
|
+
// Check if Validators.required is in the array by testing a dummy control
|
|
3041
|
+
const control = this.fb.control(null, validators);
|
|
3042
|
+
const errors = control.errors;
|
|
3043
|
+
return errors != null && 'required' in errors;
|
|
3044
|
+
}
|
|
3045
|
+
/** Store table data sources keyed by field key for template access */
|
|
3046
|
+
tableDataSources = {};
|
|
3047
|
+
/** Resolved i18n labels with defaults */
|
|
3048
|
+
get labels() {
|
|
3049
|
+
const i = this.config;
|
|
3050
|
+
const i18n = i.i18n || {};
|
|
3051
|
+
return {
|
|
3052
|
+
submit: i18n.submit || 'Submit',
|
|
3053
|
+
cancel: i18n.cancel || 'Cancel',
|
|
3054
|
+
submitting: i18n.submitting || 'Submitting...',
|
|
3055
|
+
selectPlaceholder: i18n.selectPlaceholder || 'Select...',
|
|
3056
|
+
loading: i18n.loading || 'Loading...',
|
|
3057
|
+
fileUploadPrompt: i18n.fileUploadPrompt || 'Click or drag files here',
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
ngOnInit() {
|
|
3061
|
+
this.initializeForm();
|
|
3062
|
+
this.buildRows();
|
|
3063
|
+
this.initializeVisibility();
|
|
3064
|
+
this.initializeDataSources();
|
|
3065
|
+
this.initializeTableFields();
|
|
3066
|
+
this.subscribeToValueChanges();
|
|
3067
|
+
}
|
|
3068
|
+
ngOnDestroy() {
|
|
3069
|
+
this.valueChangesSubscription?.unsubscribe();
|
|
3070
|
+
}
|
|
3071
|
+
initializeForm() {
|
|
3072
|
+
const formControls = {};
|
|
3073
|
+
this.config.fields.forEach(field => {
|
|
3074
|
+
const fieldConfig = field;
|
|
3075
|
+
let initialValue = this.config.initialValue?.[field.key] ?? null;
|
|
3076
|
+
// Handle checkbox default values
|
|
3077
|
+
if (field.kind === FieldKind.CHECKBOX && initialValue === null) {
|
|
3078
|
+
initialValue = (fieldConfig.defaultValue ?? false);
|
|
3079
|
+
}
|
|
3080
|
+
const validators = fieldConfig.validators || [];
|
|
3081
|
+
const asyncValidators = fieldConfig.asyncValidators || [];
|
|
3082
|
+
formControls[field.key] = [initialValue, validators, asyncValidators];
|
|
3083
|
+
});
|
|
3084
|
+
this.form = this.fb.group(formControls);
|
|
3085
|
+
// Apply Angular FormGroup-level validators
|
|
3086
|
+
if (this.config.groupValidators && this.config.groupValidators.length > 0) {
|
|
3087
|
+
this.form.setValidators(this.config.groupValidators);
|
|
3088
|
+
this.form.updateValueAndValidity();
|
|
3089
|
+
}
|
|
3090
|
+
// Apply disabled/readOnly state to controls
|
|
3091
|
+
this.config.fields.forEach(field => {
|
|
3092
|
+
const fieldAny = field;
|
|
3093
|
+
const control = this.form.get(field.key);
|
|
3094
|
+
if (control && (fieldAny.disabled || fieldAny.readOnly)) {
|
|
3095
|
+
control.disable();
|
|
3096
|
+
}
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
isFieldReadOnly(field) {
|
|
3100
|
+
return field.readOnly === true;
|
|
3101
|
+
}
|
|
3102
|
+
isFieldDisabled(field) {
|
|
3103
|
+
return field.disabled === true;
|
|
3104
|
+
}
|
|
3105
|
+
/** Track which field groups are currently visible */
|
|
3106
|
+
groupVisibility = {};
|
|
3107
|
+
buildRows() {
|
|
3108
|
+
if (this.config.rows && this.config.rows.length > 0) {
|
|
3109
|
+
this.rows = this.config.rows;
|
|
3110
|
+
}
|
|
3111
|
+
else if (!this.config.fieldGroups || this.config.fieldGroups.length === 0) {
|
|
3112
|
+
// Fallback: each field gets its own full-width row
|
|
3113
|
+
this.rows = this.config.fields.map(field => ({
|
|
3114
|
+
columns: 1,
|
|
3115
|
+
fields: [{ field, span: 1 }],
|
|
3116
|
+
}));
|
|
3117
|
+
}
|
|
3118
|
+
// Build field groups
|
|
3119
|
+
this.fieldGroups = this.config.fieldGroups || [];
|
|
3120
|
+
// Initialize group visibility
|
|
3121
|
+
this.initializeGroupVisibility();
|
|
3122
|
+
}
|
|
3123
|
+
initializeGroupVisibility() {
|
|
3124
|
+
this.fieldGroups.forEach((group, index) => {
|
|
3125
|
+
const key = group.title || `group-${index}`;
|
|
3126
|
+
const isVisible = group.visible ? group.visible(this.form.value) : true;
|
|
3127
|
+
this.groupVisibility[key] = isVisible;
|
|
3128
|
+
// If group is hidden, clear validators on its fields
|
|
3129
|
+
if (!isVisible) {
|
|
3130
|
+
group.fields.forEach(field => {
|
|
3131
|
+
const control = this.form.get(field.key);
|
|
3132
|
+
if (control) {
|
|
3133
|
+
control.clearValidators();
|
|
3134
|
+
control.updateValueAndValidity({ emitEvent: false });
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
updateGroupVisibility() {
|
|
3141
|
+
const formValue = this.form.value;
|
|
3142
|
+
this.fieldGroups.forEach((group, index) => {
|
|
3143
|
+
const key = group.title || `group-${index}`;
|
|
3144
|
+
const wasVisible = this.groupVisibility[key];
|
|
3145
|
+
const isVisible = group.visible ? group.visible(formValue) : true;
|
|
3146
|
+
this.groupVisibility[key] = isVisible;
|
|
3147
|
+
if (!isVisible && wasVisible) {
|
|
3148
|
+
// Group became hidden — clear validators on its fields
|
|
3149
|
+
group.fields.forEach(field => {
|
|
3150
|
+
const control = this.form.get(field.key);
|
|
3151
|
+
if (control) {
|
|
3152
|
+
control.clearValidators();
|
|
3153
|
+
control.updateValueAndValidity({ emitEvent: false });
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
else if (isVisible && !wasVisible) {
|
|
3158
|
+
// Group became visible — restore validators
|
|
3159
|
+
group.fields.forEach(field => {
|
|
3160
|
+
const fieldAny = field;
|
|
3161
|
+
const validators = fieldAny.validators || [];
|
|
3162
|
+
const control = this.form.get(field.key);
|
|
3163
|
+
if (control) {
|
|
3164
|
+
control.setValidators(validators);
|
|
3165
|
+
control.updateValueAndValidity({ emitEvent: false });
|
|
3166
|
+
}
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
isGroupVisible(group) {
|
|
3172
|
+
const key = group.title || `group-${this.fieldGroups.indexOf(group)}`;
|
|
3173
|
+
return this.groupVisibility[key] !== false;
|
|
3174
|
+
}
|
|
3175
|
+
// =========================
|
|
3176
|
+
// Feature 1: Conditional/Dynamic Fields
|
|
3177
|
+
// =========================
|
|
3178
|
+
initializeVisibility() {
|
|
3179
|
+
this.config.fields.forEach(field => {
|
|
3180
|
+
const key = field.key;
|
|
3181
|
+
const fieldAny = field;
|
|
3182
|
+
const isVisible = fieldAny.visible ? fieldAny.visible(this.form.value) : true;
|
|
3183
|
+
this.fieldVisibility[key] = isVisible;
|
|
3184
|
+
// If initially hidden, clear validators so they don't block submit
|
|
3185
|
+
if (!isVisible) {
|
|
3186
|
+
const control = this.form.get(key);
|
|
3187
|
+
if (control) {
|
|
3188
|
+
control.clearValidators();
|
|
3189
|
+
control.updateValueAndValidity({ emitEvent: false });
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
updateVisibility() {
|
|
3195
|
+
const formValue = this.form.value;
|
|
3196
|
+
this.config.fields.forEach(field => {
|
|
3197
|
+
const key = field.key;
|
|
3198
|
+
const fieldAny = field;
|
|
3199
|
+
const wasVisible = this.fieldVisibility[key];
|
|
3200
|
+
const isVisible = fieldAny.visible ? fieldAny.visible(formValue) : true;
|
|
3201
|
+
this.fieldVisibility[key] = isVisible;
|
|
3202
|
+
// When a field becomes hidden, clear its validators so it doesn't block submit
|
|
3203
|
+
const control = this.form.get(key);
|
|
3204
|
+
if (control) {
|
|
3205
|
+
if (!isVisible && wasVisible) {
|
|
3206
|
+
control.clearValidators();
|
|
3207
|
+
control.updateValueAndValidity({ emitEvent: false });
|
|
3208
|
+
}
|
|
3209
|
+
else if (isVisible && !wasVisible) {
|
|
3210
|
+
// Restore validators
|
|
3211
|
+
const validators = fieldAny.validators || [];
|
|
3212
|
+
control.setValidators(validators);
|
|
3213
|
+
control.updateValueAndValidity({ emitEvent: false });
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
isFieldVisible(field) {
|
|
3219
|
+
return this.fieldVisibility[field.key] !== false;
|
|
3220
|
+
}
|
|
3221
|
+
// =========================
|
|
3222
|
+
// Feature 2: Cross-Field Validation
|
|
3223
|
+
// =========================
|
|
3224
|
+
runFormValidators() {
|
|
3225
|
+
this.formErrors = {};
|
|
3226
|
+
if (!this.config.formValidators)
|
|
3227
|
+
return;
|
|
3228
|
+
const formValue = this.form.value;
|
|
3229
|
+
for (const validator of this.config.formValidators) {
|
|
3230
|
+
const errors = validator(formValue);
|
|
3231
|
+
if (errors) {
|
|
3232
|
+
Object.assign(this.formErrors, errors);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
getFieldError(key) {
|
|
3237
|
+
return this.formErrors[key] || null;
|
|
3238
|
+
}
|
|
3239
|
+
get hasFormErrors() {
|
|
3240
|
+
return Object.keys(this.formErrors).length > 0;
|
|
3241
|
+
}
|
|
3242
|
+
// =========================
|
|
3243
|
+
// Feature 3: Async Data Sources
|
|
3244
|
+
// =========================
|
|
3245
|
+
initializeDataSources() {
|
|
3246
|
+
this.config.fields.forEach(field => {
|
|
3247
|
+
const fieldAny = field;
|
|
3248
|
+
if (fieldAny.dataSource) {
|
|
3249
|
+
this.loadFieldOptions(field.key, fieldAny.dataSource, this.form.value);
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
async loadFieldOptions(key, dataSource, formValue) {
|
|
3254
|
+
this.fieldLoading[key] = true;
|
|
3255
|
+
try {
|
|
3256
|
+
const options = await dataSource.load(formValue);
|
|
3257
|
+
this.fieldOptions[key] = options;
|
|
3258
|
+
}
|
|
3259
|
+
catch (error) {
|
|
3260
|
+
console.error(`Failed to load options for field '${key}':`, error);
|
|
3261
|
+
this.fieldOptions[key] = [];
|
|
3262
|
+
}
|
|
3263
|
+
finally {
|
|
3264
|
+
this.fieldLoading[key] = false;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
/** Get options for a field — uses dataSource options if available, otherwise static options */
|
|
3268
|
+
getFieldOptions(field) {
|
|
3269
|
+
const key = field.key;
|
|
3270
|
+
if (this.fieldOptions[key] !== undefined) {
|
|
3271
|
+
return this.fieldOptions[key];
|
|
3272
|
+
}
|
|
3273
|
+
return field.options || [];
|
|
3274
|
+
}
|
|
3275
|
+
isFieldLoading(key) {
|
|
3276
|
+
return this.fieldLoading[key] === true;
|
|
3277
|
+
}
|
|
3278
|
+
// =========================
|
|
3279
|
+
// Feature: Multi-Select Table Fields
|
|
3280
|
+
// =========================
|
|
3281
|
+
initializeTableFields() {
|
|
3282
|
+
this.config.fields.forEach(field => {
|
|
3283
|
+
if (field.kind === FieldKind.MULTI_SELECT_TABLE) {
|
|
3284
|
+
const tableField = field;
|
|
3285
|
+
const ds = tableField.tableDataSource;
|
|
3286
|
+
// Force multi selection mode
|
|
3287
|
+
ds.selectionMode = 'multi';
|
|
3288
|
+
this.tableDataSources[field.key] = ds;
|
|
3289
|
+
// Initialize form control with empty array if null
|
|
3290
|
+
const control = this.form.get(field.key);
|
|
3291
|
+
if (control && control.value === null) {
|
|
3292
|
+
control.setValue([], { emitEvent: false });
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
else if (field.kind === FieldKind.SINGLE_SELECT_TABLE) {
|
|
3296
|
+
const tableField = field;
|
|
3297
|
+
const ds = tableField.tableDataSource;
|
|
3298
|
+
// Force single selection mode
|
|
3299
|
+
ds.selectionMode = 'single';
|
|
3300
|
+
this.tableDataSources[field.key] = ds;
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
onTableSelectionChange(field, selectedRows) {
|
|
3305
|
+
if (field.kind === FieldKind.SINGLE_SELECT_TABLE) {
|
|
3306
|
+
const tableField = field;
|
|
3307
|
+
const getVal = tableField.getRowValue || tableField.tableDataSource.getID;
|
|
3308
|
+
const value = selectedRows.length > 0 ? getVal(selectedRows[0]) : null;
|
|
3309
|
+
const control = this.form.get(field.key);
|
|
3310
|
+
if (control) {
|
|
3311
|
+
control.setValue(value);
|
|
3312
|
+
control.markAsTouched();
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
else {
|
|
3316
|
+
const tableField = field;
|
|
3317
|
+
const getVal = tableField.getRowValue || tableField.tableDataSource.getID;
|
|
3318
|
+
const values = selectedRows.map(row => getVal(row));
|
|
3319
|
+
const control = this.form.get(field.key);
|
|
3320
|
+
if (control) {
|
|
3321
|
+
control.setValue(values);
|
|
3322
|
+
control.markAsTouched();
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
// =========================
|
|
3327
|
+
// Feature: Rating Fields
|
|
3328
|
+
// =========================
|
|
3329
|
+
getRatingRange(field) {
|
|
3330
|
+
const max = field.max || 5;
|
|
3331
|
+
return Array.from({ length: max }, (_, i) => i + 1);
|
|
3332
|
+
}
|
|
3333
|
+
setRating(field, value) {
|
|
3334
|
+
const control = this.form.get(field.key);
|
|
3335
|
+
if (control) {
|
|
3336
|
+
control.setValue(value);
|
|
3337
|
+
control.markAsTouched();
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
getRatingValue(field) {
|
|
3341
|
+
return this.form.get(field.key)?.value || 0;
|
|
3342
|
+
}
|
|
3343
|
+
// =========================
|
|
3344
|
+
// Feature: Slider Fields
|
|
3345
|
+
// =========================
|
|
3346
|
+
onSliderChange(field, event) {
|
|
3347
|
+
const input = event.target;
|
|
3348
|
+
const value = parseFloat(input.value);
|
|
3349
|
+
const control = this.form.get(field.key);
|
|
3350
|
+
if (control) {
|
|
3351
|
+
control.setValue(value);
|
|
3352
|
+
control.markAsTouched();
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
getSliderValue(field) {
|
|
3356
|
+
const f = field;
|
|
3357
|
+
return this.form.get(field.key)?.value ?? f.min ?? 0;
|
|
3358
|
+
}
|
|
3359
|
+
// =========================
|
|
3360
|
+
// Feature: Color Fields
|
|
3361
|
+
// =========================
|
|
3362
|
+
onColorChange(field, event) {
|
|
3363
|
+
const input = event.target;
|
|
3364
|
+
const control = this.form.get(field.key);
|
|
3365
|
+
if (control) {
|
|
3366
|
+
control.setValue(input.value);
|
|
3367
|
+
control.markAsTouched();
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
setColorFromSwatch(field, color) {
|
|
3371
|
+
const control = this.form.get(field.key);
|
|
3372
|
+
if (control) {
|
|
3373
|
+
control.setValue(color);
|
|
3374
|
+
control.markAsTouched();
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
getColorValue(field) {
|
|
3378
|
+
return this.form.get(field.key)?.value || '#000000';
|
|
3379
|
+
}
|
|
3380
|
+
// =========================
|
|
3381
|
+
// Value Changes Subscription
|
|
3382
|
+
// =========================
|
|
3383
|
+
subscribeToValueChanges() {
|
|
3384
|
+
// Initialize previousFormValue with current form state to avoid false "changed" on first emission
|
|
3385
|
+
this.previousFormValue = { ...this.form.value };
|
|
3386
|
+
this.valueChangesSubscription = this.form.valueChanges.subscribe(formValue => {
|
|
3387
|
+
// Update conditional visibility
|
|
3388
|
+
this.updateVisibility();
|
|
3389
|
+
// Update group visibility
|
|
3390
|
+
this.updateGroupVisibility();
|
|
3391
|
+
// Run cross-field validators
|
|
3392
|
+
this.runFormValidators();
|
|
3393
|
+
// Reload data sources that depend on changed fields
|
|
3394
|
+
this.reloadDependentDataSources(formValue);
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
previousFormValue = {};
|
|
3398
|
+
reloadDependentDataSources(formValue) {
|
|
3399
|
+
this.config.fields.forEach(field => {
|
|
3400
|
+
const fieldAny = field;
|
|
3401
|
+
const dataSource = fieldAny.dataSource;
|
|
3402
|
+
if (!dataSource?.dependsOn)
|
|
3403
|
+
return;
|
|
3404
|
+
// Check if any dependency changed
|
|
3405
|
+
const changed = dataSource.dependsOn.some((depKey) => {
|
|
3406
|
+
return formValue[depKey] !== this.previousFormValue[depKey];
|
|
3407
|
+
});
|
|
3408
|
+
if (changed) {
|
|
3409
|
+
this.loadFieldOptions(field.key, dataSource, formValue);
|
|
3410
|
+
}
|
|
3411
|
+
});
|
|
3412
|
+
this.previousFormValue = { ...formValue };
|
|
3413
|
+
}
|
|
3414
|
+
getGridColumns(row) {
|
|
3415
|
+
const cols = row.columns || 1;
|
|
3416
|
+
return `repeat(${cols}, 1fr)`;
|
|
3417
|
+
}
|
|
3418
|
+
getGridSpan(rowField) {
|
|
3419
|
+
const span = rowField.span || 1;
|
|
3420
|
+
return span > 1 ? `span ${span}` : '';
|
|
3421
|
+
}
|
|
3422
|
+
// =========================
|
|
3423
|
+
// Feature: File Upload Fields
|
|
3424
|
+
// =========================
|
|
3425
|
+
/** Store selected files keyed by field key */
|
|
3426
|
+
fileSelections = {};
|
|
3427
|
+
onFileChange(field, event) {
|
|
3428
|
+
const input = event.target;
|
|
3429
|
+
const fileField = field;
|
|
3430
|
+
const files = Array.from(input.files || []);
|
|
3431
|
+
const key = field.key;
|
|
3432
|
+
// Validate file size
|
|
3433
|
+
if (fileField.maxSize) {
|
|
3434
|
+
const oversized = files.filter(f => f.size > fileField.maxSize);
|
|
3435
|
+
if (oversized.length > 0) {
|
|
3436
|
+
// Remove oversized files
|
|
3437
|
+
const valid = files.filter(f => f.size <= fileField.maxSize);
|
|
3438
|
+
this.fileSelections[key] = fileField.multiple
|
|
3439
|
+
? [...(this.fileSelections[key] || []), ...valid]
|
|
3440
|
+
: valid.slice(0, 1);
|
|
3441
|
+
}
|
|
3442
|
+
else {
|
|
3443
|
+
this.fileSelections[key] = fileField.multiple
|
|
3444
|
+
? [...(this.fileSelections[key] || []), ...files]
|
|
3445
|
+
: files.slice(0, 1);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
else {
|
|
3449
|
+
this.fileSelections[key] = fileField.multiple
|
|
3450
|
+
? [...(this.fileSelections[key] || []), ...files]
|
|
3451
|
+
: files.slice(0, 1);
|
|
3452
|
+
}
|
|
3453
|
+
// Enforce maxFiles
|
|
3454
|
+
if (fileField.maxFiles && this.fileSelections[key].length > fileField.maxFiles) {
|
|
3455
|
+
this.fileSelections[key] = this.fileSelections[key].slice(0, fileField.maxFiles);
|
|
3456
|
+
}
|
|
3457
|
+
const control = this.form.get(key);
|
|
3458
|
+
if (control) {
|
|
3459
|
+
control.setValue(this.fileSelections[key]);
|
|
3460
|
+
control.markAsTouched();
|
|
3461
|
+
}
|
|
3462
|
+
// Reset input so same file can be re-selected
|
|
3463
|
+
input.value = '';
|
|
3464
|
+
}
|
|
3465
|
+
removeFile(key, index) {
|
|
3466
|
+
this.fileSelections[key] = (this.fileSelections[key] || []).filter((_, i) => i !== index);
|
|
3467
|
+
const control = this.form.get(key);
|
|
3468
|
+
if (control) {
|
|
3469
|
+
control.setValue(this.fileSelections[key].length > 0 ? this.fileSelections[key] : null);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
getSelectedFiles(key) {
|
|
3473
|
+
return this.fileSelections[key] || [];
|
|
3474
|
+
}
|
|
3475
|
+
formatFileSize(bytes) {
|
|
3476
|
+
if (bytes < 1024)
|
|
3477
|
+
return bytes + ' B';
|
|
3478
|
+
if (bytes < 1024 * 1024)
|
|
3479
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
3480
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
3481
|
+
}
|
|
3482
|
+
getFileError(field) {
|
|
3483
|
+
const control = this.form.get(field.key);
|
|
3484
|
+
if (!control?.errors)
|
|
3485
|
+
return 'This field is required';
|
|
3486
|
+
if (control.errors['required'])
|
|
3487
|
+
return 'Please select a file';
|
|
3488
|
+
return 'Invalid file';
|
|
3489
|
+
}
|
|
3490
|
+
async submit() {
|
|
3491
|
+
// Run cross-field validators before submit
|
|
3492
|
+
this.runFormValidators();
|
|
3493
|
+
if (this.form.invalid || this.isSubmitting || this.hasFormErrors) {
|
|
3494
|
+
this.form.markAllAsTouched();
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
if (this.config.submitMode === SubmitMode.ONCE) {
|
|
3498
|
+
this.isSubmitting = true;
|
|
3499
|
+
}
|
|
3500
|
+
const formValue = this.form.value;
|
|
3501
|
+
try {
|
|
3502
|
+
if (this.config.onComplete) {
|
|
3503
|
+
await this.config.onComplete.handle(formValue);
|
|
3504
|
+
}
|
|
3505
|
+
this.modalRef.close(formValue);
|
|
3506
|
+
}
|
|
3507
|
+
catch (error) {
|
|
3508
|
+
// Always allow retry on error, regardless of submit mode
|
|
3509
|
+
this.isSubmitting = false;
|
|
3510
|
+
console.error('Form submission error:', error);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
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 });
|
|
3514
|
+
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"] }] });
|
|
3515
|
+
}
|
|
3516
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
|
|
3517
|
+
type: Component,
|
|
3518
|
+
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"] }]
|
|
3519
|
+
}], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
|
|
3520
|
+
type: Input
|
|
3521
|
+
}], modalRef: [{
|
|
3522
|
+
type: Input
|
|
3523
|
+
}], hideFooter: [{
|
|
3524
|
+
type: Input
|
|
3525
|
+
}] } });
|
|
3526
|
+
|
|
3527
|
+
class MnWizardBodyComponent {
|
|
3528
|
+
cdr;
|
|
3529
|
+
config;
|
|
3530
|
+
modalRef;
|
|
3531
|
+
formBodies;
|
|
3532
|
+
currentStepId;
|
|
3533
|
+
visitedStepIds = [];
|
|
3534
|
+
isCurrentStepValid = true;
|
|
3535
|
+
isCompleting = false;
|
|
3536
|
+
wizardErrors = {};
|
|
3537
|
+
/** Resolved i18n labels with defaults */
|
|
3538
|
+
get labels() {
|
|
3539
|
+
const i18n = this.config.i18n || {};
|
|
3540
|
+
return {
|
|
3541
|
+
next: i18n.next || 'Next',
|
|
3542
|
+
back: i18n.back || 'Back',
|
|
3543
|
+
close: i18n.close || 'Close',
|
|
3544
|
+
complete: i18n.complete || 'Complete',
|
|
3545
|
+
completing: i18n.completing || 'Completing...',
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
/** Pre-built form configs keyed by step id — only for steps that have fields */
|
|
3549
|
+
stepFormConfigs = {};
|
|
3550
|
+
statusSubscription;
|
|
3551
|
+
formBodiesSubscription;
|
|
3552
|
+
constructor(cdr) {
|
|
3553
|
+
this.cdr = cdr;
|
|
3554
|
+
}
|
|
3555
|
+
ngOnInit() {
|
|
3556
|
+
// Pre-build form configs for all form-driven steps
|
|
3557
|
+
for (const step of this.config.steps) {
|
|
3558
|
+
if (step.fields && step.fields.length > 0) {
|
|
3559
|
+
this.stepFormConfigs[step.id] = {
|
|
3560
|
+
kind: ModalKind.FORM,
|
|
3561
|
+
fields: step.fields,
|
|
3562
|
+
rows: step.rows,
|
|
3563
|
+
fieldGroups: step.fieldGroups,
|
|
3564
|
+
formValidators: step.formValidators,
|
|
3565
|
+
groupValidators: step.groupValidators,
|
|
3566
|
+
initialValue: step.initialValue,
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
this.currentStepId = this.config.startStepId || this.config.steps[0]?.id;
|
|
3571
|
+
if (this.currentStepId) {
|
|
3572
|
+
this.visitedStepIds.push(this.currentStepId);
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
ngAfterViewInit() {
|
|
3576
|
+
// Subscribe to form bodies list changes to track validity
|
|
3577
|
+
this.formBodiesSubscription = this.formBodies.changes.subscribe(() => {
|
|
3578
|
+
this.trackCurrentStepValidity();
|
|
3579
|
+
});
|
|
3580
|
+
// Initial validity check
|
|
3581
|
+
this.trackCurrentStepValidity();
|
|
3582
|
+
}
|
|
3583
|
+
ngOnDestroy() {
|
|
3584
|
+
this.statusSubscription?.unsubscribe();
|
|
3585
|
+
this.formBodiesSubscription?.unsubscribe();
|
|
3586
|
+
}
|
|
3587
|
+
asAny(val) {
|
|
3588
|
+
return val;
|
|
3589
|
+
}
|
|
3590
|
+
isTextBody(step) {
|
|
3591
|
+
return typeof step.body === 'string' || typeof step.body === 'number';
|
|
3592
|
+
}
|
|
3593
|
+
/** Get visible steps (filtered by visibility condition) */
|
|
3594
|
+
get visibleSteps() {
|
|
3595
|
+
return this.config.steps.filter(s => this.isStepVisible(s));
|
|
3596
|
+
}
|
|
3597
|
+
isStepVisible(step) {
|
|
3598
|
+
if (!step.visible)
|
|
3599
|
+
return true;
|
|
3600
|
+
return step.visible(this.getAggregatedData());
|
|
3601
|
+
}
|
|
3602
|
+
get currentStep() {
|
|
3603
|
+
return this.config.steps.find(s => s.id === this.currentStepId);
|
|
3604
|
+
}
|
|
3605
|
+
get currentVisibleIndex() {
|
|
3606
|
+
return this.visibleSteps.findIndex(s => s.id === this.currentStepId);
|
|
3607
|
+
}
|
|
3608
|
+
get currentStepIndex() {
|
|
3609
|
+
return this.config.steps.findIndex(s => s.id === this.currentStepId);
|
|
3610
|
+
}
|
|
3611
|
+
get canGoBack() {
|
|
3612
|
+
return this.currentVisibleIndex > 0;
|
|
3613
|
+
}
|
|
3614
|
+
get canGoNext() {
|
|
3615
|
+
return this.currentVisibleIndex < this.visibleSteps.length - 1;
|
|
3616
|
+
}
|
|
3617
|
+
get isLastStep() {
|
|
3618
|
+
return this.currentVisibleIndex === this.visibleSteps.length - 1;
|
|
3619
|
+
}
|
|
3620
|
+
get isFreeFlow() {
|
|
3621
|
+
return this.config.flow === WizardFlowMode.FREE;
|
|
3622
|
+
}
|
|
3623
|
+
canNavigateToStep(step) {
|
|
3624
|
+
if (!this.isFreeFlow)
|
|
3625
|
+
return false;
|
|
3626
|
+
return this.isStepVisible(step);
|
|
3627
|
+
}
|
|
3628
|
+
async goToStep(step) {
|
|
3629
|
+
if (!this.canNavigateToStep(step))
|
|
3630
|
+
return;
|
|
3631
|
+
if (step.id === this.currentStepId)
|
|
3632
|
+
return;
|
|
3633
|
+
const previousStepId = this.currentStepId;
|
|
3634
|
+
this.currentStepId = step.id;
|
|
3635
|
+
if (!this.visitedStepIds.includes(this.currentStepId)) {
|
|
3636
|
+
this.visitedStepIds.push(this.currentStepId);
|
|
3637
|
+
}
|
|
3638
|
+
await this.notifyStepChange(previousStepId, NavigationDirection.DIRECT);
|
|
3639
|
+
this.trackCurrentStepValidity();
|
|
3640
|
+
}
|
|
3641
|
+
/** Find the MnFormBodyComponent for the current step */
|
|
3642
|
+
getCurrentFormBody() {
|
|
3643
|
+
if (!this.formBodies)
|
|
3644
|
+
return undefined;
|
|
3645
|
+
const stepIndex = this.currentStepIndex;
|
|
3646
|
+
// formBodies only contains entries for steps that have stepFormConfigs
|
|
3647
|
+
// We need to find which form body index corresponds to the current step
|
|
3648
|
+
const formStepIds = this.config.steps
|
|
3649
|
+
.filter(s => this.stepFormConfigs[s.id])
|
|
3650
|
+
.map(s => s.id);
|
|
3651
|
+
const formIndex = formStepIds.indexOf(this.currentStepId);
|
|
3652
|
+
if (formIndex === -1)
|
|
3653
|
+
return undefined;
|
|
3654
|
+
return this.formBodies.toArray()[formIndex];
|
|
3655
|
+
}
|
|
3656
|
+
trackCurrentStepValidity() {
|
|
3657
|
+
// Unsubscribe from previous
|
|
3658
|
+
this.statusSubscription?.unsubscribe();
|
|
3659
|
+
this.statusSubscription = undefined;
|
|
3660
|
+
const formBody = this.getCurrentFormBody();
|
|
3661
|
+
if (formBody && formBody.form) {
|
|
3662
|
+
this.isCurrentStepValid = formBody.form.valid;
|
|
3663
|
+
this.statusSubscription = formBody.form.statusChanges.subscribe(() => {
|
|
3664
|
+
this.isCurrentStepValid = formBody.form.valid;
|
|
3665
|
+
this.cdr.detectChanges();
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
else {
|
|
3669
|
+
this.isCurrentStepValid = true;
|
|
3670
|
+
}
|
|
3671
|
+
this.cdr.detectChanges();
|
|
3672
|
+
}
|
|
3673
|
+
async next() {
|
|
3674
|
+
if (!this.canGoNext)
|
|
3675
|
+
return;
|
|
3676
|
+
const currentStep = this.currentStep;
|
|
3677
|
+
// Validate embedded form if present
|
|
3678
|
+
const formBody = this.getCurrentFormBody();
|
|
3679
|
+
if (formBody && formBody.form.invalid) {
|
|
3680
|
+
formBody.form.markAllAsTouched();
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
if (currentStep?.guard?.canExit) {
|
|
3684
|
+
const canExit = await currentStep.guard.canExit();
|
|
3685
|
+
if (!canExit)
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
if (currentStep?.validators) {
|
|
3689
|
+
for (const validator of currentStep.validators) {
|
|
3690
|
+
const result = await validator.validate();
|
|
3691
|
+
if (result.status === 'invalid')
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
// Find next visible step
|
|
3696
|
+
const nextStep = this.visibleSteps[this.currentVisibleIndex + 1];
|
|
3697
|
+
if (!nextStep)
|
|
3698
|
+
return;
|
|
3699
|
+
if (nextStep.guard?.canEnter) {
|
|
3700
|
+
const canEnter = await nextStep.guard.canEnter();
|
|
3701
|
+
if (!canEnter)
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
const previousStepId = this.currentStepId;
|
|
3705
|
+
this.currentStepId = nextStep.id;
|
|
3706
|
+
if (!this.visitedStepIds.includes(this.currentStepId)) {
|
|
3707
|
+
this.visitedStepIds.push(this.currentStepId);
|
|
3708
|
+
}
|
|
3709
|
+
await this.notifyStepChange(previousStepId, NavigationDirection.FORWARD);
|
|
3710
|
+
this.trackCurrentStepValidity();
|
|
3711
|
+
}
|
|
3712
|
+
async back() {
|
|
3713
|
+
if (!this.canGoBack) {
|
|
3714
|
+
// Call onCancel handler if configured
|
|
3715
|
+
if (this.config.onCancel) {
|
|
3716
|
+
await this.config.onCancel(ModalCloseReason.CANCELLED);
|
|
3717
|
+
}
|
|
3718
|
+
this.modalRef.dismiss(ModalCloseReason.CANCELLED);
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
const prevStep = this.visibleSteps[this.currentVisibleIndex - 1];
|
|
3722
|
+
const previousStepId = this.currentStepId;
|
|
3723
|
+
this.currentStepId = prevStep.id;
|
|
3724
|
+
await this.notifyStepChange(previousStepId, NavigationDirection.BACKWARD);
|
|
3725
|
+
this.trackCurrentStepValidity();
|
|
3726
|
+
}
|
|
3727
|
+
async complete() {
|
|
3728
|
+
if (this.isCompleting)
|
|
3729
|
+
return;
|
|
3730
|
+
const currentStep = this.currentStep;
|
|
3731
|
+
// Validate embedded form if present
|
|
3732
|
+
const formBody = this.getCurrentFormBody();
|
|
3733
|
+
if (formBody && formBody.form.invalid) {
|
|
3734
|
+
formBody.form.markAllAsTouched();
|
|
3735
|
+
return;
|
|
3736
|
+
}
|
|
3737
|
+
if (currentStep?.validators) {
|
|
3738
|
+
for (const validator of currentStep.validators) {
|
|
3739
|
+
const result = await validator.validate();
|
|
3740
|
+
if (result.status === 'invalid')
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
this.isCompleting = true;
|
|
3745
|
+
this.wizardErrors = {};
|
|
3746
|
+
this.cdr.detectChanges();
|
|
3747
|
+
try {
|
|
3748
|
+
const aggregatedData = this.getAggregatedData();
|
|
3749
|
+
// Run cross-step validators (onBeforeComplete)
|
|
3750
|
+
if (this.config.onBeforeComplete) {
|
|
3751
|
+
for (const validator of this.config.onBeforeComplete) {
|
|
3752
|
+
const errors = await validator(aggregatedData);
|
|
3753
|
+
if (errors) {
|
|
3754
|
+
this.wizardErrors = { ...this.wizardErrors, ...errors };
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
if (Object.keys(this.wizardErrors).length > 0) {
|
|
3758
|
+
this.isCompleting = false;
|
|
3759
|
+
this.cdr.detectChanges();
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
const result = {
|
|
3764
|
+
status: ModalCloseReason.COMPLETED,
|
|
3765
|
+
visitedStepIds: this.visitedStepIds,
|
|
3766
|
+
payload: aggregatedData,
|
|
3767
|
+
};
|
|
3768
|
+
if (this.config.onComplete) {
|
|
3769
|
+
await this.config.onComplete.handle(result);
|
|
3770
|
+
}
|
|
3771
|
+
this.modalRef.close(result);
|
|
3772
|
+
}
|
|
3773
|
+
catch (error) {
|
|
3774
|
+
this.isCompleting = false;
|
|
3775
|
+
this.cdr.detectChanges();
|
|
3776
|
+
console.error('Wizard completion error:', error);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
/** Collect form data from all form-driven steps, namespaced by step ID */
|
|
3780
|
+
getAggregatedData() {
|
|
3781
|
+
const aggregated = {};
|
|
3782
|
+
if (!this.formBodies)
|
|
3783
|
+
return aggregated;
|
|
3784
|
+
const formStepIds = this.config.steps
|
|
3785
|
+
.filter(s => this.stepFormConfigs[s.id])
|
|
3786
|
+
.map(s => s.id);
|
|
3787
|
+
this.formBodies.toArray().forEach((fb, index) => {
|
|
3788
|
+
if (fb.form && formStepIds[index]) {
|
|
3789
|
+
aggregated[formStepIds[index]] = { ...fb.form.value };
|
|
3790
|
+
}
|
|
3791
|
+
});
|
|
3792
|
+
return aggregated;
|
|
3793
|
+
}
|
|
3794
|
+
async notifyStepChange(previousStepId, direction) {
|
|
3795
|
+
if (this.config.onStepChange) {
|
|
3796
|
+
await this.config.onStepChange.handle({
|
|
3797
|
+
previousStepId,
|
|
3798
|
+
currentStepId: this.currentStepId,
|
|
3799
|
+
direction,
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
3804
|
+
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" }] });
|
|
3805
|
+
}
|
|
3806
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
|
|
3807
|
+
type: Component,
|
|
3808
|
+
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" }]
|
|
3809
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
|
|
3810
|
+
type: Input
|
|
3811
|
+
}], modalRef: [{
|
|
3812
|
+
type: Input
|
|
3813
|
+
}], formBodies: [{
|
|
3814
|
+
type: ViewChildren,
|
|
3815
|
+
args: [MnFormBodyComponent]
|
|
3816
|
+
}] } });
|
|
3817
|
+
|
|
3818
|
+
class MnConfirmationBodyComponent {
|
|
3819
|
+
config;
|
|
3820
|
+
modalRef;
|
|
3821
|
+
async confirm() {
|
|
3822
|
+
const result = true;
|
|
3823
|
+
if (this.config.confirm?.handler) {
|
|
3824
|
+
await this.config.confirm.handler.handle(result);
|
|
3825
|
+
}
|
|
3826
|
+
this.modalRef.close(result);
|
|
3827
|
+
}
|
|
3828
|
+
cancel() {
|
|
3829
|
+
const reason = this.config.cancel?.reason || ModalCloseReason.CANCELLED;
|
|
3830
|
+
this.modalRef.dismiss(reason);
|
|
3831
|
+
}
|
|
3832
|
+
get confirmLabel() {
|
|
3833
|
+
return this.config.confirm?.label || 'Confirm';
|
|
3834
|
+
}
|
|
3835
|
+
get cancelLabel() {
|
|
3836
|
+
return this.config.cancel?.label || 'Cancel';
|
|
3837
|
+
}
|
|
3838
|
+
get confirmStyle() {
|
|
3839
|
+
return this.config.confirm?.style || ActionStyle.PRIMARY;
|
|
3840
|
+
}
|
|
3841
|
+
get cancelStyle() {
|
|
3842
|
+
return this.config.cancel?.style || ActionStyle.SECONDARY;
|
|
3843
|
+
}
|
|
3844
|
+
get toneClass() {
|
|
3845
|
+
switch (this.config.tone) {
|
|
3846
|
+
case ConfirmationTone.WARNING:
|
|
3847
|
+
return 'tone-warning';
|
|
3848
|
+
case ConfirmationTone.DANGER:
|
|
3849
|
+
return 'tone-danger';
|
|
3850
|
+
default:
|
|
3851
|
+
return 'tone-default';
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
getButtonColor(style) {
|
|
3855
|
+
switch (style) {
|
|
3856
|
+
case ActionStyle.PRIMARY:
|
|
3857
|
+
return 'primary';
|
|
3858
|
+
case ActionStyle.DANGER:
|
|
3859
|
+
return 'danger';
|
|
3860
|
+
case ActionStyle.GHOST:
|
|
3861
|
+
return 'secondary';
|
|
3862
|
+
default:
|
|
3863
|
+
return 'secondary';
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
getButtonVariant(style) {
|
|
3867
|
+
switch (style) {
|
|
3868
|
+
case ActionStyle.PRIMARY:
|
|
3869
|
+
case ActionStyle.DANGER:
|
|
3870
|
+
return 'fill';
|
|
3871
|
+
case ActionStyle.GHOST:
|
|
3872
|
+
return 'text';
|
|
3873
|
+
default:
|
|
3874
|
+
return 'outline';
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3878
|
+
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" }, ngImport: i0, 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", 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"] }] });
|
|
3879
|
+
}
|
|
3880
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
|
|
3881
|
+
type: Component,
|
|
3882
|
+
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" }]
|
|
3883
|
+
}], propDecorators: { config: [{
|
|
3884
|
+
type: Input
|
|
3885
|
+
}], modalRef: [{
|
|
3886
|
+
type: Input
|
|
3887
|
+
}] } });
|
|
3888
|
+
|
|
3889
|
+
class MnCustomBodyHostComponent {
|
|
3890
|
+
config;
|
|
3891
|
+
modalRef;
|
|
3892
|
+
container;
|
|
3893
|
+
componentRef;
|
|
3894
|
+
ngOnInit() {
|
|
3895
|
+
setTimeout(() => this.loadContent(), 0);
|
|
3896
|
+
}
|
|
3897
|
+
loadContent() {
|
|
3898
|
+
if (!this.container)
|
|
3899
|
+
return;
|
|
3900
|
+
this.container.clear();
|
|
3901
|
+
if (this.config.component) {
|
|
3902
|
+
this.attachComponent(this.config.component);
|
|
3903
|
+
}
|
|
3904
|
+
else if (this.config.template) {
|
|
3905
|
+
this.attachTemplate(this.config.template);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
attachComponent(component) {
|
|
3909
|
+
this.componentRef = this.container.createComponent(component);
|
|
3910
|
+
// Pass inputs to the component
|
|
3911
|
+
if (this.config.inputs) {
|
|
3912
|
+
Object.entries(this.config.inputs).forEach(([key, value]) => {
|
|
3913
|
+
this.componentRef.instance[key] = value;
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
// Pass modalRef if the component has a modalRef property
|
|
3917
|
+
const instance = this.componentRef.instance;
|
|
3918
|
+
if (instance && 'modalRef' in instance) {
|
|
3919
|
+
instance.modalRef = this.modalRef;
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
attachTemplate(template) {
|
|
3923
|
+
this.container.createEmbeddedView(template, {
|
|
3924
|
+
$implicit: this.modalRef,
|
|
3925
|
+
modalRef: this.modalRef,
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3929
|
+
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 }] });
|
|
3930
|
+
}
|
|
3931
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, decorators: [{
|
|
3932
|
+
type: Component,
|
|
3933
|
+
args: [{
|
|
3934
|
+
selector: 'mn-custom-body-host',
|
|
3935
|
+
standalone: true,
|
|
3936
|
+
imports: [CommonModule],
|
|
3937
|
+
template: '<ng-container #container></ng-container>',
|
|
3938
|
+
}]
|
|
3939
|
+
}], propDecorators: { config: [{
|
|
3940
|
+
type: Input
|
|
3941
|
+
}], modalRef: [{
|
|
3942
|
+
type: Input
|
|
3943
|
+
}], container: [{
|
|
3944
|
+
type: ViewChild,
|
|
3945
|
+
args: ['container', { read: ViewContainerRef }]
|
|
3946
|
+
}] } });
|
|
3947
|
+
|
|
3948
|
+
class MnModalShellComponent {
|
|
3949
|
+
el;
|
|
3950
|
+
config;
|
|
3951
|
+
modalRef;
|
|
3952
|
+
isClosing = false;
|
|
3953
|
+
ModalKind = ModalKind;
|
|
3954
|
+
previouslyFocusedElement = null;
|
|
3955
|
+
focusTrapListener = null;
|
|
3956
|
+
pollingTimer = null;
|
|
3957
|
+
pollAttempts = 0;
|
|
3958
|
+
constructor(el) {
|
|
3959
|
+
this.el = el;
|
|
3960
|
+
}
|
|
3961
|
+
ngOnInit() {
|
|
3962
|
+
this.startPollingIfConfigured();
|
|
3963
|
+
}
|
|
3964
|
+
ngAfterViewInit() {
|
|
3965
|
+
this.previouslyFocusedElement = document.activeElement;
|
|
3966
|
+
this.setupFocusTrap();
|
|
3967
|
+
// Focus the modal container
|
|
3968
|
+
const container = this.el.nativeElement.querySelector('.modal-container');
|
|
3969
|
+
if (container) {
|
|
3970
|
+
container.focus();
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
ngOnDestroy() {
|
|
3974
|
+
this.removeFocusTrap();
|
|
3975
|
+
this.stopPolling();
|
|
3976
|
+
// Restore focus to previously focused element
|
|
3977
|
+
if (this.previouslyFocusedElement && typeof this.previouslyFocusedElement.focus === 'function') {
|
|
3978
|
+
this.previouslyFocusedElement.focus();
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
setupFocusTrap() {
|
|
3982
|
+
this.focusTrapListener = (e) => {
|
|
3983
|
+
if (e.key !== 'Tab')
|
|
3984
|
+
return;
|
|
3985
|
+
const focusable = this.el.nativeElement.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
|
|
3986
|
+
if (focusable.length === 0)
|
|
3987
|
+
return;
|
|
3988
|
+
const first = focusable[0];
|
|
3989
|
+
const last = focusable[focusable.length - 1];
|
|
3990
|
+
if (e.shiftKey) {
|
|
3991
|
+
if (document.activeElement === first) {
|
|
3992
|
+
e.preventDefault();
|
|
3993
|
+
last.focus();
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
else {
|
|
3997
|
+
if (document.activeElement === last) {
|
|
3998
|
+
e.preventDefault();
|
|
3999
|
+
first.focus();
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
this.el.nativeElement.addEventListener('keydown', this.focusTrapListener);
|
|
4004
|
+
}
|
|
4005
|
+
removeFocusTrap() {
|
|
4006
|
+
if (this.focusTrapListener) {
|
|
4007
|
+
this.el.nativeElement.removeEventListener('keydown', this.focusTrapListener);
|
|
4008
|
+
this.focusTrapListener = null;
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
asWizard(config) {
|
|
4012
|
+
return config;
|
|
4013
|
+
}
|
|
4014
|
+
asForm(config) {
|
|
4015
|
+
return config;
|
|
4016
|
+
}
|
|
4017
|
+
asConfirmation(config) {
|
|
4018
|
+
return config;
|
|
4019
|
+
}
|
|
4020
|
+
asCustom(config) {
|
|
4021
|
+
return config;
|
|
4022
|
+
}
|
|
4023
|
+
asAny(val) {
|
|
4024
|
+
return val;
|
|
4025
|
+
}
|
|
4026
|
+
get hostClasses() {
|
|
4027
|
+
const size = this.config.size || ModalSize.MD;
|
|
4028
|
+
const closing = this.isClosing ? ' closing' : '';
|
|
4029
|
+
return `modal-shell modal-${size}${closing}`;
|
|
4030
|
+
}
|
|
4031
|
+
startClosing() {
|
|
4032
|
+
this.isClosing = true;
|
|
4033
|
+
return new Promise(resolve => setTimeout(resolve, 150));
|
|
4034
|
+
}
|
|
4035
|
+
onEscapeKey(event) {
|
|
4036
|
+
if (this.config.keyboard === KeyboardMode.ENABLED) {
|
|
4037
|
+
this.handleClose(ModalCloseReason.ESCAPE);
|
|
4038
|
+
if (event && event.preventDefault) {
|
|
4039
|
+
event.preventDefault();
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
onBackdropClick() {
|
|
4044
|
+
if (this.config.backdrop === BackdropMode.CLOSABLE) {
|
|
4045
|
+
this.handleClose(ModalCloseReason.BACKDROP);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
onCloseButtonClick() {
|
|
4049
|
+
this.handleClose(ModalCloseReason.DISMISSED);
|
|
4050
|
+
}
|
|
4051
|
+
async handleClose(reason) {
|
|
4052
|
+
if (this.config.closeMode === CloseMode.DISABLED) {
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
if (this.config.closeMode === CloseMode.GUARDED) {
|
|
4056
|
+
if (this.config.closeGuard) {
|
|
4057
|
+
const allowed = await this.config.closeGuard();
|
|
4058
|
+
if (!allowed)
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
this.modalRef.dismiss(reason);
|
|
4063
|
+
}
|
|
4064
|
+
get showBackdrop() {
|
|
4065
|
+
return this.config.backdrop !== BackdropMode.HIDE;
|
|
4066
|
+
}
|
|
4067
|
+
get containerSizeClass() {
|
|
4068
|
+
switch (this.config.size || ModalSize.MD) {
|
|
4069
|
+
case ModalSize.SM: return 'w-96';
|
|
4070
|
+
case ModalSize.MD: return 'w-[32rem]';
|
|
4071
|
+
case ModalSize.LG: return 'w-[48rem]';
|
|
4072
|
+
case ModalSize.XL: return 'w-[64rem]';
|
|
4073
|
+
case ModalSize.FULL: return 'w-[95vw] h-[95vh] max-h-[95vh]';
|
|
4074
|
+
default: return 'w-[32rem]';
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
get showCloseButton() {
|
|
4078
|
+
return this.config.closeMode !== CloseMode.DISABLED;
|
|
4079
|
+
}
|
|
4080
|
+
// =========================
|
|
4081
|
+
// Footer Actions
|
|
4082
|
+
// =========================
|
|
4083
|
+
get hasCustomFooterActions() {
|
|
4084
|
+
return !!this.config.footerActions && this.config.footerActions.length > 0;
|
|
4085
|
+
}
|
|
4086
|
+
get leftFooterActions() {
|
|
4087
|
+
return (this.config.footerActions || []).filter(a => a.position === 'left');
|
|
4088
|
+
}
|
|
4089
|
+
get rightFooterActions() {
|
|
4090
|
+
return (this.config.footerActions || []).filter(a => a.position !== 'left');
|
|
4091
|
+
}
|
|
4092
|
+
async onFooterAction(action) {
|
|
4093
|
+
if (action.disabled)
|
|
4094
|
+
return;
|
|
4095
|
+
if (action.handler) {
|
|
4096
|
+
await action.handler(this.modalRef);
|
|
4097
|
+
}
|
|
4098
|
+
if (action.closesModal) {
|
|
4099
|
+
if (action.closeReason === ModalCloseReason.COMPLETED) {
|
|
4100
|
+
this.modalRef.close();
|
|
4101
|
+
}
|
|
4102
|
+
else {
|
|
4103
|
+
this.modalRef.dismiss(action.closeReason || ModalCloseReason.DISMISSED);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
getActionButtonColor(style) {
|
|
4108
|
+
switch (style) {
|
|
4109
|
+
case ActionStyle.PRIMARY: return 'primary';
|
|
4110
|
+
case ActionStyle.DANGER: return 'danger';
|
|
4111
|
+
case ActionStyle.GHOST: return 'secondary';
|
|
4112
|
+
default: return 'secondary';
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
getActionButtonVariant(style) {
|
|
4116
|
+
switch (style) {
|
|
4117
|
+
case ActionStyle.PRIMARY:
|
|
4118
|
+
case ActionStyle.DANGER: return 'fill';
|
|
4119
|
+
case ActionStyle.GHOST: return 'text';
|
|
4120
|
+
default: return 'outline';
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
// =========================
|
|
4124
|
+
// Polling
|
|
4125
|
+
// =========================
|
|
4126
|
+
startPollingIfConfigured() {
|
|
4127
|
+
const polling = this.config.polling;
|
|
4128
|
+
if (!polling)
|
|
4129
|
+
return;
|
|
4130
|
+
if (polling.autoStart === false)
|
|
4131
|
+
return;
|
|
4132
|
+
this.startPolling();
|
|
4133
|
+
}
|
|
4134
|
+
startPolling() {
|
|
4135
|
+
const polling = this.config.polling;
|
|
4136
|
+
if (!polling)
|
|
4137
|
+
return;
|
|
4138
|
+
this.pollingTimer = setInterval(async () => {
|
|
4139
|
+
this.pollAttempts++;
|
|
4140
|
+
try {
|
|
4141
|
+
const shouldStop = await polling.onPoll(this.modalRef);
|
|
4142
|
+
if (shouldStop === true) {
|
|
4143
|
+
this.stopPolling();
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
catch (e) {
|
|
4147
|
+
console.error('Polling error:', e);
|
|
4148
|
+
}
|
|
4149
|
+
if (polling.maxAttempts && this.pollAttempts >= polling.maxAttempts) {
|
|
4150
|
+
this.stopPolling();
|
|
4151
|
+
}
|
|
4152
|
+
}, polling.interval);
|
|
4153
|
+
}
|
|
4154
|
+
stopPolling() {
|
|
4155
|
+
if (this.pollingTimer) {
|
|
4156
|
+
clearInterval(this.pollingTimer);
|
|
4157
|
+
this.pollingTimer = null;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
4161
|
+
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 animate-[slideIn_0.2s_ease-in-out]\"\n [ngClass]=\"containerSizeClass\"\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}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing) .modal-container{animation:slideOut .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"] }, { 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"] }] });
|
|
4162
|
+
}
|
|
4163
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
4164
|
+
type: Component,
|
|
4165
|
+
args: [{ selector: 'mn-modal-shell', standalone: true, imports: [
|
|
4166
|
+
CommonModule,
|
|
4167
|
+
MnWizardBodyComponent,
|
|
4168
|
+
MnFormBodyComponent,
|
|
4169
|
+
MnConfirmationBodyComponent,
|
|
4170
|
+
MnCustomBodyHostComponent,
|
|
4171
|
+
MnButton,
|
|
4172
|
+
], 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 animate-[slideIn_0.2s_ease-in-out]\"\n [ngClass]=\"containerSizeClass\"\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}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing) .modal-container{animation:slideOut .15s ease-in-out forwards}\n"] }]
|
|
4173
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { config: [{
|
|
4174
|
+
type: Input
|
|
4175
|
+
}], modalRef: [{
|
|
4176
|
+
type: Input
|
|
4177
|
+
}], hostClasses: [{
|
|
4178
|
+
type: HostBinding,
|
|
4179
|
+
args: ['class']
|
|
4180
|
+
}], onEscapeKey: [{
|
|
4181
|
+
type: HostListener,
|
|
4182
|
+
args: ['document:keydown.escape', ['$event']]
|
|
4183
|
+
}] } });
|
|
4184
|
+
|
|
4185
|
+
class MnModalService {
|
|
4186
|
+
appRef = inject(ApplicationRef);
|
|
4187
|
+
injector = inject(EnvironmentInjector);
|
|
4188
|
+
open(config) {
|
|
4189
|
+
// Create the modal shell component
|
|
4190
|
+
const componentRef = createComponent(MnModalShellComponent, {
|
|
4191
|
+
environmentInjector: this.injector,
|
|
4192
|
+
});
|
|
4193
|
+
// Set the config on the component
|
|
4194
|
+
componentRef.instance.config = config;
|
|
4195
|
+
// Create modal ref
|
|
4196
|
+
const modalRef = new MnModalRef(componentRef, config);
|
|
4197
|
+
componentRef.instance.modalRef = modalRef;
|
|
4198
|
+
// Attach to application
|
|
4199
|
+
this.appRef.attachView(componentRef.hostView);
|
|
4200
|
+
const domElem = componentRef.location.nativeElement;
|
|
4201
|
+
document.body.appendChild(domElem);
|
|
4202
|
+
// Clean up on close
|
|
4203
|
+
modalRef.afterClosed$.subscribe(() => {
|
|
4204
|
+
this.appRef.detachView(componentRef.hostView);
|
|
4205
|
+
domElem.remove();
|
|
4206
|
+
});
|
|
4207
|
+
return modalRef;
|
|
4208
|
+
}
|
|
4209
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4210
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, providedIn: 'root' });
|
|
4211
|
+
}
|
|
4212
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, decorators: [{
|
|
4213
|
+
type: Injectable,
|
|
4214
|
+
args: [{
|
|
4215
|
+
providedIn: 'root',
|
|
4216
|
+
}]
|
|
4217
|
+
}] });
|
|
4218
|
+
|
|
4219
|
+
// Types
|
|
4220
|
+
|
|
4221
|
+
class MnSectionDirective {
|
|
4222
|
+
/** Section name contributed by this DOM node to the section path */
|
|
4223
|
+
mnSection;
|
|
4224
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4225
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnSectionDirective, isStandalone: true, selector: "[mn-section]", inputs: { mnSection: ["mn-section", "mnSection"] }, providers: [
|
|
4226
|
+
{
|
|
4227
|
+
provide: MN_SECTION_PATH,
|
|
4228
|
+
// Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
|
|
4229
|
+
// and read the attribute value using Attribute so it's available at provider creation time.
|
|
4230
|
+
deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
|
|
4231
|
+
useFactory: (parentPath, attr) => {
|
|
4232
|
+
const parent = Array.isArray(parentPath) ? parentPath : [];
|
|
4233
|
+
const name = (attr ?? '').trim();
|
|
4234
|
+
return name ? [...parent, name] : [...parent];
|
|
4235
|
+
},
|
|
4236
|
+
},
|
|
4237
|
+
], ngImport: i0 });
|
|
4238
|
+
}
|
|
4239
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, decorators: [{
|
|
4240
|
+
type: Directive,
|
|
4241
|
+
args: [{
|
|
4242
|
+
selector: '[mn-section]',
|
|
4243
|
+
standalone: true,
|
|
4244
|
+
providers: [
|
|
4245
|
+
{
|
|
4246
|
+
provide: MN_SECTION_PATH,
|
|
4247
|
+
// Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
|
|
4248
|
+
// and read the attribute value using Attribute so it's available at provider creation time.
|
|
4249
|
+
deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
|
|
4250
|
+
useFactory: (parentPath, attr) => {
|
|
4251
|
+
const parent = Array.isArray(parentPath) ? parentPath : [];
|
|
4252
|
+
const name = (attr ?? '').trim();
|
|
4253
|
+
return name ? [...parent, name] : [...parent];
|
|
4254
|
+
},
|
|
4255
|
+
},
|
|
4256
|
+
],
|
|
4257
|
+
}]
|
|
4258
|
+
}], propDecorators: { mnSection: [{
|
|
4259
|
+
type: Input,
|
|
4260
|
+
args: ['mn-section']
|
|
4261
|
+
}] } });
|
|
4262
|
+
|
|
4263
|
+
class MnInstanceDirective {
|
|
4264
|
+
/** Instance id for targeting per-component instance overrides */
|
|
4265
|
+
mnInstance;
|
|
4266
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4267
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnInstanceDirective, isStandalone: true, selector: "[mn-instance]", inputs: { mnInstance: ["mn-instance", "mnInstance"] }, providers: [
|
|
4268
|
+
{
|
|
4269
|
+
provide: MN_INSTANCE_ID,
|
|
4270
|
+
// Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
|
|
4271
|
+
deps: [new Attribute('mn-instance')],
|
|
4272
|
+
useFactory: (attr) => (attr ?? '').trim() || null,
|
|
4273
|
+
},
|
|
4274
|
+
], ngImport: i0 });
|
|
4275
|
+
}
|
|
4276
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, decorators: [{
|
|
4277
|
+
type: Directive,
|
|
4278
|
+
args: [{
|
|
4279
|
+
selector: '[mn-instance]',
|
|
4280
|
+
standalone: true,
|
|
4281
|
+
providers: [
|
|
4282
|
+
{
|
|
4283
|
+
provide: MN_INSTANCE_ID,
|
|
4284
|
+
// Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
|
|
4285
|
+
deps: [new Attribute('mn-instance')],
|
|
4286
|
+
useFactory: (attr) => (attr ?? '').trim() || null,
|
|
4287
|
+
},
|
|
4288
|
+
],
|
|
4289
|
+
}]
|
|
4290
|
+
}], propDecorators: { mnInstance: [{
|
|
4291
|
+
type: Input,
|
|
4292
|
+
args: ['mn-instance']
|
|
4293
|
+
}] } });
|
|
4294
|
+
|
|
4295
|
+
/**
|
|
4296
|
+
* Provides an APP_INITIALIZER that configures the MnLanguageService and
|
|
4297
|
+
* preloads the requested locales during application bootstrap.
|
|
4298
|
+
*
|
|
4299
|
+
* Usage in app.config.ts:
|
|
4300
|
+
* ...provideMnLanguage({
|
|
4301
|
+
* urlPattern: 'assets/i18n/{locale}.json',
|
|
4302
|
+
* defaultLocale: 'en',
|
|
4303
|
+
* preload: ['en', 'nl'],
|
|
4304
|
+
* })
|
|
4305
|
+
*/
|
|
4306
|
+
function provideMnLanguage(config) {
|
|
4307
|
+
return [
|
|
4308
|
+
{
|
|
4309
|
+
provide: APP_INITIALIZER,
|
|
4310
|
+
multi: true,
|
|
4311
|
+
useFactory: (svc) => async () => {
|
|
4312
|
+
svc.configure(config.urlPattern);
|
|
4313
|
+
const localesToLoad = config.preload ?? [config.defaultLocale];
|
|
4314
|
+
await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
|
|
4315
|
+
await svc.setLocale(config.defaultLocale);
|
|
4316
|
+
},
|
|
4317
|
+
deps: [MnLanguageService],
|
|
4318
|
+
},
|
|
4319
|
+
];
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
/**
|
|
4323
|
+
* Pipe that translates a key via MnLanguageService.
|
|
4324
|
+
*
|
|
4325
|
+
* Usage in templates:
|
|
4326
|
+
* {{ 'form.email.label' | mnTranslate }}
|
|
4327
|
+
* {{ 'greeting' | mnTranslate:{ name: 'World' } }}
|
|
4328
|
+
*
|
|
4329
|
+
* Note: This pipe is impure so it re-evaluates when the locale changes.
|
|
4330
|
+
*/
|
|
4331
|
+
class MnTranslatePipe {
|
|
4332
|
+
lang;
|
|
4333
|
+
constructor(lang) {
|
|
4334
|
+
this.lang = lang;
|
|
4335
|
+
}
|
|
4336
|
+
transform(key, params) {
|
|
4337
|
+
return this.lang.translate(key, params);
|
|
4338
|
+
}
|
|
4339
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [{ token: MnLanguageService }], target: i0.ɵɵFactoryTarget.Pipe });
|
|
4340
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
|
|
4341
|
+
}
|
|
4342
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
|
|
4343
|
+
type: Pipe,
|
|
4344
|
+
args: [{
|
|
4345
|
+
name: 'mnTranslate',
|
|
4346
|
+
standalone: true,
|
|
4347
|
+
pure: false,
|
|
4348
|
+
}]
|
|
4349
|
+
}], ctorParameters: () => [{ type: MnLanguageService }] });
|
|
4350
|
+
|
|
1468
4351
|
/*
|
|
1469
4352
|
* Public API Surface of mn-lib
|
|
1470
4353
|
*/
|
|
@@ -1473,5 +4356,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1473
4356
|
* Generated bundle index. Do not edit.
|
|
1474
4357
|
*/
|
|
1475
4358
|
|
|
1476
|
-
export { DEFAULT_MN_ALERT_CONFIG, MN_ALERT_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnConfigService, MnDualHorizontalImage, MnInformationCard, MnInputField, MnInstanceDirective, MnSectionDirective, MnTestComponent, MnTextarea, Test, dateTimeAdapter, defaultTextAdapter, mnAlertVariants, mnButtonVariants, mnInformationCardVariants, mnInputFieldVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig };
|
|
4359
|
+
export { ActionStyle, BackdropMode, BaseModalBuilder, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, 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 };
|
|
1477
4360
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|