mn-angular-lib 0.0.51 → 0.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, DestroyRef, Self, APP_INITIALIZER,
|
|
2
|
+
import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, signal, ElementRef, DestroyRef, Self, APP_INITIALIZER, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewContainerRef, ViewChild, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
|
|
3
3
|
export { TemplateRef, Type } from '@angular/core';
|
|
4
|
-
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, map, catchError
|
|
4
|
+
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgClass, NgOptimizedImage, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
|
7
7
|
import { tv } from 'tailwind-variants';
|
|
8
8
|
import * as i1$2 from '@angular/forms';
|
|
9
|
-
import { Validators, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
|
9
|
+
import { Validators, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
|
10
10
|
import * as i1$1 from '@angular/common/http';
|
|
11
11
|
import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
|
|
12
12
|
import JSON5 from 'json5';
|
|
@@ -401,12 +401,65 @@ const emptyToNull = (raw) => (raw === '' ? null : raw);
|
|
|
401
401
|
* - Values are stored as strings in the FormControl
|
|
402
402
|
* - No special DOM attributes
|
|
403
403
|
* - No additional validation (relies on Angular's built-in validators)
|
|
404
|
+
* - Supports simple masking (0 for digit, A for alpha, * for any)
|
|
404
405
|
*/
|
|
405
406
|
const defaultTextAdapter = {
|
|
406
407
|
parse: (raw) => emptyToNull(raw),
|
|
407
408
|
format: (val) => (val == null ? '' : String(val)),
|
|
408
409
|
attrs: () => ({}),
|
|
409
410
|
validate: () => null,
|
|
411
|
+
applyMask: (value, mask) => {
|
|
412
|
+
if (!mask || !value)
|
|
413
|
+
return value;
|
|
414
|
+
let result = '';
|
|
415
|
+
let maskIndex = 0;
|
|
416
|
+
let dataIndex = 0;
|
|
417
|
+
// Remove non-alphanumeric if we want to re-mask from clean data
|
|
418
|
+
// But usually we just want to restrict input.
|
|
419
|
+
// A simple implementation:
|
|
420
|
+
while (maskIndex < mask.length && dataIndex < value.length) {
|
|
421
|
+
const maskChar = mask[maskIndex];
|
|
422
|
+
const dataChar = value[dataIndex];
|
|
423
|
+
if (maskChar === '0') {
|
|
424
|
+
if (/\d/.test(dataChar)) {
|
|
425
|
+
result += dataChar;
|
|
426
|
+
dataIndex++;
|
|
427
|
+
maskIndex++;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
dataIndex++; // skip invalid
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (maskChar === 'A') {
|
|
434
|
+
if (/[a-zA-Z]/.test(dataChar)) {
|
|
435
|
+
result += dataChar;
|
|
436
|
+
dataIndex++;
|
|
437
|
+
maskIndex++;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
dataIndex++; // skip invalid
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
else if (maskChar === '*') {
|
|
444
|
+
result += dataChar;
|
|
445
|
+
dataIndex++;
|
|
446
|
+
maskIndex++;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
result += maskChar;
|
|
450
|
+
if (dataChar === maskChar) {
|
|
451
|
+
dataIndex++;
|
|
452
|
+
}
|
|
453
|
+
maskIndex++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Auto-append static characters if next in mask
|
|
457
|
+
while (maskIndex < mask.length && !/[0A*]/.test(mask[maskIndex])) {
|
|
458
|
+
result += mask[maskIndex];
|
|
459
|
+
maskIndex++;
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
410
463
|
};
|
|
411
464
|
/**
|
|
412
465
|
* Adapter for date and time input types.
|
|
@@ -667,11 +720,19 @@ function isPlainObject(value) {
|
|
|
667
720
|
class MnConfigService {
|
|
668
721
|
http;
|
|
669
722
|
_config = null;
|
|
723
|
+
_settings = {};
|
|
670
724
|
_debugMode = false;
|
|
725
|
+
/** Reactive version counter — incremented on every config load. */
|
|
726
|
+
_configVersion = signal(0, ...(ngDevMode ? [{ debugName: "_configVersion" }] : []));
|
|
727
|
+
configVersion = this._configVersion.asReadonly();
|
|
671
728
|
lang = inject(MnLanguageService);
|
|
672
729
|
constructor(http) {
|
|
673
730
|
this.http = http;
|
|
674
731
|
}
|
|
732
|
+
/** General settings from the config file (version, name, etc.). */
|
|
733
|
+
get settings() {
|
|
734
|
+
return this._settings;
|
|
735
|
+
}
|
|
675
736
|
/**
|
|
676
737
|
* Load the configuration JSON from the provided URL and cache it in memory.
|
|
677
738
|
* Consumers should typically call this via the APP_INITIALIZER helper.
|
|
@@ -698,7 +759,9 @@ class MnConfigService {
|
|
|
698
759
|
const cfg = (isPlainObject(json) ? json : {});
|
|
699
760
|
const defaults = isPlainObject(cfg.defaults) ? cfg.defaults : {};
|
|
700
761
|
const overrides = isPlainObject(cfg.overrides) ? cfg.overrides : cfg.overrides ?? {};
|
|
701
|
-
|
|
762
|
+
const settings = isPlainObject(cfg.settings) ? cfg.settings : {};
|
|
763
|
+
this._config = { settings, defaults, overrides };
|
|
764
|
+
this._settings = settings;
|
|
702
765
|
// Bootstrap language service from config if a "language" section is present.
|
|
703
766
|
// This avoids circular dependency: config reads raw language settings and
|
|
704
767
|
// pushes them into the language service (language service never imports config).
|
|
@@ -711,6 +774,31 @@ class MnConfigService {
|
|
|
711
774
|
await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
|
|
712
775
|
await this.lang.setLocale(effectiveLocale);
|
|
713
776
|
}
|
|
777
|
+
this._configVersion.update(v => v + 1);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Load configuration from a pre-parsed object (no HTTP fetch).
|
|
781
|
+
* Used for live preview scenarios where config is pushed via postMessage.
|
|
782
|
+
* Optionally re-bootstraps the language service if a `language` section is present.
|
|
783
|
+
*/
|
|
784
|
+
async loadFromObject(config, bootstrapLanguage = false) {
|
|
785
|
+
const defaults = isPlainObject(config['defaults']) ? config['defaults'] : {};
|
|
786
|
+
const overrides = isPlainObject(config['overrides']) ? config['overrides'] : {};
|
|
787
|
+
const settings = isPlainObject(config['settings']) ? config['settings'] : {};
|
|
788
|
+
this._config = { settings, defaults, overrides };
|
|
789
|
+
this._settings = settings;
|
|
790
|
+
if (bootstrapLanguage) {
|
|
791
|
+
const langCfg = config['language'];
|
|
792
|
+
if (isPlainObject(langCfg) && typeof langCfg['urlPattern'] === 'string') {
|
|
793
|
+
const lc = langCfg;
|
|
794
|
+
this.lang.configure(lc.urlPattern);
|
|
795
|
+
const effectiveLocale = this.lang.resolveLocaleForDomain(lc.domainLocaleMap, lc.defaultLocale);
|
|
796
|
+
const localesToLoad = lc.preload ?? [effectiveLocale];
|
|
797
|
+
await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
|
|
798
|
+
await this.lang.setLocale(effectiveLocale);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
this._configVersion.update(v => v + 1);
|
|
714
802
|
}
|
|
715
803
|
/**
|
|
716
804
|
* Resolve a configuration object for a component, optionally scoped to a section path
|
|
@@ -865,6 +953,7 @@ class MnInputField {
|
|
|
865
953
|
ngControl;
|
|
866
954
|
/** Resolved UI configuration for the input field */
|
|
867
955
|
uiConfig = {};
|
|
956
|
+
el = inject(ElementRef);
|
|
868
957
|
/** Configuration properties for the input field */
|
|
869
958
|
props;
|
|
870
959
|
configService = inject(MnConfigService);
|
|
@@ -910,6 +999,17 @@ class MnInputField {
|
|
|
910
999
|
this.resolveConfig();
|
|
911
1000
|
});
|
|
912
1001
|
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
1002
|
+
if (this.props.autoFocus) {
|
|
1003
|
+
setTimeout(() => this.focus(), 0);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Focuses the input element.
|
|
1008
|
+
*/
|
|
1009
|
+
focus() {
|
|
1010
|
+
const input = this.el.nativeElement.querySelector('input');
|
|
1011
|
+
if (input)
|
|
1012
|
+
input.focus();
|
|
913
1013
|
}
|
|
914
1014
|
resolveConfig() {
|
|
915
1015
|
const instanceId = this.explicitInstanceId || `mn-input-${this.props.id}`;
|
|
@@ -971,8 +1071,13 @@ class MnInputField {
|
|
|
971
1071
|
* @param raw - Raw string value from the input element
|
|
972
1072
|
*/
|
|
973
1073
|
handleInput(raw) {
|
|
974
|
-
|
|
975
|
-
|
|
1074
|
+
let finalValue = raw;
|
|
1075
|
+
// Apply mask if available
|
|
1076
|
+
if (this.props.mask && typeof this.adapter.applyMask === 'function') {
|
|
1077
|
+
finalValue = this.adapter.applyMask(raw, this.props.mask);
|
|
1078
|
+
}
|
|
1079
|
+
this.value = finalValue;
|
|
1080
|
+
this.onChange(this.adapter.parse(finalValue));
|
|
976
1081
|
}
|
|
977
1082
|
/**
|
|
978
1083
|
* Handles blur events from the input element.
|
|
@@ -1127,11 +1232,11 @@ class MnInputField {
|
|
|
1127
1232
|
});
|
|
1128
1233
|
}
|
|
1129
1234
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
1130
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [
|
|
1235
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnInputField, isStandalone: true, selector: "mn-lib-input-field", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n\n }\n }\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
|
|
1131
1236
|
}
|
|
1132
1237
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInputField, decorators: [{
|
|
1133
1238
|
type: Component,
|
|
1134
|
-
args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [
|
|
1239
|
+
args: [{ selector: 'mn-lib-input-field', standalone: true, imports: [CommonModule, NgClass, MnErrorMessage, FormsModule], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Input Element -->\n <input\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [type]=\"props.type\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.min]=\"minAttr\"\n [attr.max]=\"maxAttr\"\n [ngModel]=\"value\"\n [ngClass]=\"inputClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n />\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n\n }\n }\n }\n</div>\n" }]
|
|
1135
1240
|
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
1136
1241
|
type: Optional
|
|
1137
1242
|
}, {
|
|
@@ -1374,6 +1479,7 @@ class MnTextarea {
|
|
|
1374
1479
|
ngControl;
|
|
1375
1480
|
/** Resolved UI configuration for the textarea */
|
|
1376
1481
|
uiConfig = {};
|
|
1482
|
+
el = inject(ElementRef);
|
|
1377
1483
|
/** Configuration properties for the textarea */
|
|
1378
1484
|
props;
|
|
1379
1485
|
configService = inject(MnConfigService);
|
|
@@ -1416,6 +1522,17 @@ class MnTextarea {
|
|
|
1416
1522
|
this.resolveConfig();
|
|
1417
1523
|
});
|
|
1418
1524
|
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
1525
|
+
if (this.props.autoFocus) {
|
|
1526
|
+
setTimeout(() => this.focus(), 0);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Focuses the textarea element.
|
|
1531
|
+
*/
|
|
1532
|
+
focus() {
|
|
1533
|
+
const textarea = this.el.nativeElement.querySelector('textarea');
|
|
1534
|
+
if (textarea)
|
|
1535
|
+
textarea.focus();
|
|
1419
1536
|
}
|
|
1420
1537
|
resolveConfig() {
|
|
1421
1538
|
const instanceId = this.explicitInstanceId || `mn-textarea-${this.props.id}`;
|
|
@@ -1581,11 +1698,11 @@ class MnTextarea {
|
|
|
1581
1698
|
});
|
|
1582
1699
|
}
|
|
1583
1700
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTextarea, deps: [{ token: i1$2.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
1584
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTextarea, isStandalone: true, selector: "mn-lib-textarea", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
1701
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTextarea, isStandalone: true, selector: "mn-lib-textarea", inputs: { props: "props" }, ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnErrorMessage, selector: "lib-mn-error-message", inputs: ["errorMessage", "id"] }] });
|
|
1585
1702
|
}
|
|
1586
1703
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTextarea, decorators: [{
|
|
1587
1704
|
type: Component,
|
|
1588
|
-
args: [{ selector: 'mn-lib-textarea', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
1705
|
+
args: [{ selector: 'mn-lib-textarea', standalone: true, imports: [NgClass, MnErrorMessage], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n <!-- Label -->\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500 \">*</span>\n }\n </label>\n }\n\n <!-- Textarea Element -->\n <textarea\n [id]=\"resolvedId\"\n [attr.name]=\"resolvedName\"\n [attr.placeholder]=\"uiConfig.placeholder || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [disabled]=\"isDisabled\"\n [attr.autocomplete]=\"props.autocomplete || null\"\n [attr.rows]=\"props.rows ?? null\"\n [attr.cols]=\"props.cols ?? null\"\n [ngClass]=\"textareaClasses\"\n (input)=\"handleInput(($any($event.target)).value)\"\n (blur)=\"handleBlur()\"\n >{{ value ?? '' }}</textarea>\n\n <!-- Error Messages -->\n @if (showError) {\n <!-- Show all errors mode -->\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1\">\n @for (error of errorMessages; track $index) {\n <lib-mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></lib-mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage != null) {\n <lib-mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></lib-mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
1589
1706
|
}], ctorParameters: () => [{ type: i1$2.NgControl, decorators: [{
|
|
1590
1707
|
type: Optional
|
|
1591
1708
|
}, {
|
|
@@ -2307,53 +2424,41 @@ var ValidationCode;
|
|
|
2307
2424
|
ValidationCode["CUSTOM"] = "custom";
|
|
2308
2425
|
})(ValidationCode || (ValidationCode = {}));
|
|
2309
2426
|
|
|
2310
|
-
|
|
2427
|
+
/**
|
|
2428
|
+
* A builder class that provides form layout capabilities (fields, rows, groups).
|
|
2429
|
+
* This can be used as a delegate to avoid code duplication between FormModalBuilder and StepBuilder.
|
|
2430
|
+
*/
|
|
2431
|
+
class FormLayoutBuilder {
|
|
2311
2432
|
config;
|
|
2433
|
+
parent;
|
|
2312
2434
|
currentRow = [];
|
|
2313
2435
|
currentRowColumns = 1;
|
|
2314
|
-
constructor(
|
|
2315
|
-
this.config =
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
state: StepState.PENDING,
|
|
2320
|
-
};
|
|
2436
|
+
constructor(config, parent) {
|
|
2437
|
+
this.config = config;
|
|
2438
|
+
this.parent = parent;
|
|
2439
|
+
this.config.fields = this.config.fields || [];
|
|
2440
|
+
this.config.rows = this.config.rows || [];
|
|
2321
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Add a custom body/content to the form/step.
|
|
2444
|
+
*/
|
|
2322
2445
|
body(body) {
|
|
2323
2446
|
this.config.body = body;
|
|
2324
|
-
return this;
|
|
2325
|
-
}
|
|
2326
|
-
state(state) {
|
|
2327
|
-
this.config.state = state;
|
|
2328
|
-
return this;
|
|
2329
|
-
}
|
|
2330
|
-
guard(guard) {
|
|
2331
|
-
this.config.guard = guard;
|
|
2332
|
-
return this;
|
|
2333
|
-
}
|
|
2334
|
-
validators(validators) {
|
|
2335
|
-
this.config.validators = validators;
|
|
2336
|
-
return this;
|
|
2447
|
+
return this.parent;
|
|
2337
2448
|
}
|
|
2338
2449
|
/**
|
|
2339
|
-
* Add a field
|
|
2340
|
-
*
|
|
2341
|
-
* @example
|
|
2342
|
-
* s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
|
|
2343
|
-
* s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
|
|
2450
|
+
* Add a field as a full-width row (single column).
|
|
2344
2451
|
*/
|
|
2345
2452
|
field(field) {
|
|
2346
2453
|
this.flushCurrentRow();
|
|
2347
2454
|
this.config.fields = this.config.fields || [];
|
|
2348
2455
|
this.config.fields.push(field);
|
|
2349
|
-
|
|
2350
|
-
this.config.rows = [];
|
|
2351
|
-
}
|
|
2456
|
+
this.config.rows = this.config.rows || [];
|
|
2352
2457
|
this.config.rows.push({
|
|
2353
2458
|
columns: 1,
|
|
2354
2459
|
fields: [{ field, span: 1 }],
|
|
2355
2460
|
});
|
|
2356
|
-
return this;
|
|
2461
|
+
return this.parent;
|
|
2357
2462
|
}
|
|
2358
2463
|
/**
|
|
2359
2464
|
* Start a new row with the specified number of columns.
|
|
@@ -2362,70 +2467,293 @@ class StepBuilder {
|
|
|
2362
2467
|
row(columns = 2) {
|
|
2363
2468
|
this.flushCurrentRow();
|
|
2364
2469
|
this.currentRowColumns = columns;
|
|
2365
|
-
return this;
|
|
2470
|
+
return this.parent;
|
|
2366
2471
|
}
|
|
2367
2472
|
/**
|
|
2368
2473
|
* Add a field to the current row started by `row()`.
|
|
2474
|
+
* @param field - The field configuration
|
|
2475
|
+
* @param span - How many columns this field should span (default: 1)
|
|
2369
2476
|
*/
|
|
2370
2477
|
addToRow(field, span = 1) {
|
|
2371
2478
|
this.config.fields = this.config.fields || [];
|
|
2372
2479
|
this.config.fields.push(field);
|
|
2373
2480
|
this.currentRow.push({ field, span });
|
|
2374
|
-
return this;
|
|
2481
|
+
return this.parent;
|
|
2375
2482
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2483
|
+
/**
|
|
2484
|
+
* Declarative way to add a row.
|
|
2485
|
+
* @example
|
|
2486
|
+
* .addRow(2, row => {
|
|
2487
|
+
* row.add({ kind: FieldKind.TEXT, key: 'first', ... });
|
|
2488
|
+
* row.add({ kind: FieldKind.TEXT, key: 'last', ... });
|
|
2489
|
+
* })
|
|
2490
|
+
*/
|
|
2491
|
+
addRow(columns, buildFn) {
|
|
2492
|
+
this.flushCurrentRow();
|
|
2493
|
+
const rowFields = [];
|
|
2494
|
+
buildFn({
|
|
2495
|
+
add: (field, span = 1) => {
|
|
2496
|
+
this.config.fields = this.config.fields || [];
|
|
2497
|
+
this.config.fields.push(field);
|
|
2498
|
+
rowFields.push({ field, span });
|
|
2499
|
+
},
|
|
2500
|
+
});
|
|
2501
|
+
if (rowFields.length > 0) {
|
|
2502
|
+
this.config.rows = this.config.rows || [];
|
|
2381
2503
|
this.config.rows.push({
|
|
2382
|
-
columns
|
|
2383
|
-
fields:
|
|
2504
|
+
columns,
|
|
2505
|
+
fields: rowFields,
|
|
2384
2506
|
});
|
|
2385
|
-
this.currentRow = [];
|
|
2386
|
-
this.currentRowColumns = 1;
|
|
2387
2507
|
}
|
|
2508
|
+
return this.parent;
|
|
2388
2509
|
}
|
|
2389
|
-
|
|
2390
|
-
* Add a field group with a section header.
|
|
2391
|
-
*/
|
|
2392
|
-
fieldGroup(group) {
|
|
2510
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2393
2511
|
this.flushCurrentRow();
|
|
2512
|
+
if (typeof arg1 !== 'string') {
|
|
2513
|
+
const group = arg1;
|
|
2514
|
+
this.processFieldGroup(group);
|
|
2515
|
+
return this.parent;
|
|
2516
|
+
}
|
|
2517
|
+
const title = arg1;
|
|
2518
|
+
let description;
|
|
2519
|
+
let buildFn;
|
|
2520
|
+
if (typeof arg2 === 'string') {
|
|
2521
|
+
description = arg2;
|
|
2522
|
+
buildFn = arg3;
|
|
2523
|
+
}
|
|
2524
|
+
else {
|
|
2525
|
+
buildFn = arg2;
|
|
2526
|
+
}
|
|
2527
|
+
const groupConfig = {
|
|
2528
|
+
fields: [],
|
|
2529
|
+
rows: [],
|
|
2530
|
+
};
|
|
2531
|
+
const groupBuilder = new FormLayoutBuilder(groupConfig, {});
|
|
2532
|
+
// Overwrite the groupBuilder's parent to itself for proper chaining within the group
|
|
2533
|
+
groupBuilder.parent = groupBuilder;
|
|
2534
|
+
buildFn(groupBuilder);
|
|
2535
|
+
groupBuilder.flushCurrentRow();
|
|
2536
|
+
const group = {
|
|
2537
|
+
title,
|
|
2538
|
+
description,
|
|
2539
|
+
fields: groupConfig.fields || [],
|
|
2540
|
+
rows: groupConfig.rows || [],
|
|
2541
|
+
};
|
|
2542
|
+
this.processFieldGroup(group);
|
|
2543
|
+
return this.parent;
|
|
2544
|
+
}
|
|
2545
|
+
processFieldGroup(group) {
|
|
2394
2546
|
if (!this.config.fieldGroups) {
|
|
2395
2547
|
this.config.fieldGroups = [];
|
|
2396
2548
|
}
|
|
2549
|
+
// Also add group fields to the flat fields array for form control creation
|
|
2397
2550
|
this.config.fields = this.config.fields || [];
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2551
|
+
const fields = this.config.fields;
|
|
2552
|
+
group.fields.forEach((f) => fields.push(f));
|
|
2553
|
+
// Build rows for the group if not provided
|
|
2554
|
+
if (!group.rows || group.rows.length === 0) {
|
|
2555
|
+
group.rows = group.fields.map((f) => ({
|
|
2401
2556
|
columns: 1,
|
|
2402
2557
|
fields: [{ field: f, span: 1 }],
|
|
2403
2558
|
}));
|
|
2404
2559
|
}
|
|
2405
2560
|
this.config.fieldGroups.push(group);
|
|
2406
|
-
return this;
|
|
2407
2561
|
}
|
|
2408
2562
|
/**
|
|
2409
|
-
* Add form-level validators for cross-field validation
|
|
2563
|
+
* Add form-level validators for cross-field validation.
|
|
2410
2564
|
*/
|
|
2411
2565
|
formValidators(validators) {
|
|
2412
2566
|
this.config.formValidators = validators;
|
|
2413
|
-
return this;
|
|
2567
|
+
return this.parent;
|
|
2414
2568
|
}
|
|
2415
2569
|
/**
|
|
2416
|
-
* Add Angular FormGroup-level validators
|
|
2570
|
+
* Add Angular FormGroup-level validators.
|
|
2417
2571
|
*/
|
|
2418
2572
|
groupValidators(validators) {
|
|
2419
2573
|
this.config.groupValidators = validators;
|
|
2420
|
-
return this;
|
|
2574
|
+
return this.parent;
|
|
2421
2575
|
}
|
|
2422
2576
|
/**
|
|
2423
|
-
* Set initial
|
|
2577
|
+
* Set initial value for fields.
|
|
2424
2578
|
*/
|
|
2425
2579
|
initialValue(value) {
|
|
2426
2580
|
this.config.initialValue = value;
|
|
2581
|
+
return this.parent;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Set the field to be focused when the form initializes.
|
|
2585
|
+
*/
|
|
2586
|
+
focus(key) {
|
|
2587
|
+
// Clear autoFocus from other fields first to ensure only one is focused
|
|
2588
|
+
this.config.fields?.forEach(f => {
|
|
2589
|
+
f.autoFocus = false;
|
|
2590
|
+
});
|
|
2591
|
+
this.config.fieldGroups?.forEach(g => {
|
|
2592
|
+
g.fields.forEach(f => {
|
|
2593
|
+
f.autoFocus = false;
|
|
2594
|
+
});
|
|
2595
|
+
});
|
|
2596
|
+
const field = this.config.fields?.find(f => f.key === key);
|
|
2597
|
+
if (field) {
|
|
2598
|
+
field.autoFocus = true;
|
|
2599
|
+
}
|
|
2600
|
+
return this.parent;
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* Wraps a field with a fluent API for validation.
|
|
2604
|
+
*/
|
|
2605
|
+
fieldWithValidators(field) {
|
|
2606
|
+
const fieldAny = field;
|
|
2607
|
+
fieldAny.validators = fieldAny.validators || [];
|
|
2608
|
+
this.field(field);
|
|
2609
|
+
return new FieldValidatorBuilder(field, this.parent);
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Flushes any pending fields in the current row to the configuration.
|
|
2613
|
+
*/
|
|
2614
|
+
flushCurrentRow() {
|
|
2615
|
+
if (this.currentRow.length > 0) {
|
|
2616
|
+
this.config.rows.push({
|
|
2617
|
+
columns: this.currentRowColumns,
|
|
2618
|
+
fields: [...this.currentRow],
|
|
2619
|
+
});
|
|
2620
|
+
this.currentRow = [];
|
|
2621
|
+
this.currentRowColumns = 1;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* A builder for adding validation rules to a field fluently.
|
|
2627
|
+
*/
|
|
2628
|
+
class FieldValidatorBuilder {
|
|
2629
|
+
field;
|
|
2630
|
+
parent;
|
|
2631
|
+
constructor(field, parent) {
|
|
2632
|
+
this.field = field;
|
|
2633
|
+
this.parent = parent;
|
|
2634
|
+
this.field.validators = this.field.validators || [];
|
|
2635
|
+
}
|
|
2636
|
+
required(message) {
|
|
2637
|
+
this.field.validators.push(Validators.required);
|
|
2638
|
+
return this;
|
|
2639
|
+
}
|
|
2640
|
+
minLength(length) {
|
|
2641
|
+
this.field.validators.push(Validators.minLength(length));
|
|
2642
|
+
return this;
|
|
2643
|
+
}
|
|
2644
|
+
maxLength(length) {
|
|
2645
|
+
this.field.validators.push(Validators.maxLength(length));
|
|
2646
|
+
return this;
|
|
2647
|
+
}
|
|
2648
|
+
pattern(pattern) {
|
|
2649
|
+
this.field.validators.push(Validators.pattern(pattern));
|
|
2650
|
+
return this;
|
|
2651
|
+
}
|
|
2652
|
+
email() {
|
|
2653
|
+
this.field.validators.push(Validators.email);
|
|
2654
|
+
return this;
|
|
2655
|
+
}
|
|
2656
|
+
min(value) {
|
|
2657
|
+
this.field.validators.push(Validators.min(value));
|
|
2658
|
+
return this;
|
|
2659
|
+
}
|
|
2660
|
+
max(value) {
|
|
2661
|
+
this.field.validators.push(Validators.max(value));
|
|
2662
|
+
return this;
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Add a custom validator.
|
|
2666
|
+
*/
|
|
2667
|
+
custom(validator) {
|
|
2668
|
+
this.field.validators.push(validator);
|
|
2669
|
+
return this;
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Return to the parent builder.
|
|
2673
|
+
*/
|
|
2674
|
+
done() {
|
|
2675
|
+
return this.parent;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
class StepBuilder {
|
|
2680
|
+
config;
|
|
2681
|
+
layoutBuilder;
|
|
2682
|
+
constructor(id, title) {
|
|
2683
|
+
this.config = {
|
|
2684
|
+
id,
|
|
2685
|
+
title,
|
|
2686
|
+
fields: [],
|
|
2687
|
+
state: StepState.PENDING,
|
|
2688
|
+
};
|
|
2689
|
+
this.layoutBuilder = new FormLayoutBuilder(this.config, this);
|
|
2690
|
+
}
|
|
2691
|
+
body(body) {
|
|
2692
|
+
this.config.body = body;
|
|
2693
|
+
return this;
|
|
2694
|
+
}
|
|
2695
|
+
state(state) {
|
|
2696
|
+
this.config.state = state;
|
|
2697
|
+
return this;
|
|
2698
|
+
}
|
|
2699
|
+
guard(guard) {
|
|
2700
|
+
this.config.guard = guard;
|
|
2701
|
+
return this;
|
|
2702
|
+
}
|
|
2703
|
+
validators(validators) {
|
|
2704
|
+
this.config.validators = validators;
|
|
2427
2705
|
return this;
|
|
2428
2706
|
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Add a field to this step. This is the single API for all field types.
|
|
2709
|
+
*
|
|
2710
|
+
* @example
|
|
2711
|
+
* s.field({ kind: FieldKind.TEXT, key: 'email', label: 'Email', validators: [Validators.required] })
|
|
2712
|
+
* s.field({ kind: FieldKind.SELECT, key: 'role', label: 'Role', options: [...] })
|
|
2713
|
+
*/
|
|
2714
|
+
field(field) {
|
|
2715
|
+
return this.layoutBuilder.field(field);
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Start a new row with the specified number of columns.
|
|
2719
|
+
* All subsequent `addToRow()` calls will add fields to this row.
|
|
2720
|
+
*/
|
|
2721
|
+
row(columns = 2) {
|
|
2722
|
+
return this.layoutBuilder.row(columns);
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Add a field to the current row started by `row()`.
|
|
2726
|
+
*/
|
|
2727
|
+
addToRow(field, span = 1) {
|
|
2728
|
+
return this.layoutBuilder.addToRow(field, span);
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Declarative way to add a row.
|
|
2732
|
+
*/
|
|
2733
|
+
addRow(columns, buildFn) {
|
|
2734
|
+
return this.layoutBuilder.addRow(columns, buildFn);
|
|
2735
|
+
}
|
|
2736
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2737
|
+
return this.layoutBuilder.fieldGroup(arg1, arg2, arg3);
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* Add form-level validators for cross-field validation within this step.
|
|
2741
|
+
*/
|
|
2742
|
+
formValidators(validators) {
|
|
2743
|
+
return this.layoutBuilder.formValidators(validators);
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Add Angular FormGroup-level validators for this step.
|
|
2747
|
+
*/
|
|
2748
|
+
groupValidators(validators) {
|
|
2749
|
+
return this.layoutBuilder.groupValidators(validators);
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Set initial values for fields in this step.
|
|
2753
|
+
*/
|
|
2754
|
+
initialValue(value) {
|
|
2755
|
+
return this.layoutBuilder.initialValue(value);
|
|
2756
|
+
}
|
|
2429
2757
|
/**
|
|
2430
2758
|
* Set a visibility condition for this step based on aggregated wizard data.
|
|
2431
2759
|
*/
|
|
@@ -2433,16 +2761,40 @@ class StepBuilder {
|
|
|
2433
2761
|
this.config.visible = condition;
|
|
2434
2762
|
return this;
|
|
2435
2763
|
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Set a custom label for the 'Next' button on this step.
|
|
2766
|
+
*/
|
|
2767
|
+
nextLabel(label) {
|
|
2768
|
+
this.config.nextLabel = label;
|
|
2769
|
+
return this;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Set a custom label for the 'Back' button on this step.
|
|
2773
|
+
*/
|
|
2774
|
+
backLabel(label) {
|
|
2775
|
+
this.config.backLabel = label;
|
|
2776
|
+
return this;
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Hide the 'Back' button on this step.
|
|
2780
|
+
*/
|
|
2781
|
+
hideBack(hide = true) {
|
|
2782
|
+
this.config.hideBack = hide;
|
|
2783
|
+
return this;
|
|
2784
|
+
}
|
|
2436
2785
|
build() {
|
|
2437
|
-
this.flushCurrentRow();
|
|
2786
|
+
this.layoutBuilder.flushCurrentRow();
|
|
2438
2787
|
return this.config;
|
|
2439
2788
|
}
|
|
2440
2789
|
}
|
|
2441
2790
|
|
|
2442
2791
|
class BaseModalBuilder {
|
|
2443
2792
|
config;
|
|
2793
|
+
layoutBuilder;
|
|
2444
2794
|
constructor(initialConfig) {
|
|
2445
2795
|
this.config = initialConfig;
|
|
2796
|
+
// Base layout builder, we pass this.config directly if it supports it
|
|
2797
|
+
this.layoutBuilder = new FormLayoutBuilder(this.config, this);
|
|
2446
2798
|
}
|
|
2447
2799
|
title(title) {
|
|
2448
2800
|
this.config.title = title;
|
|
@@ -2480,6 +2832,14 @@ class BaseModalBuilder {
|
|
|
2480
2832
|
this.config.intent = intent;
|
|
2481
2833
|
return this;
|
|
2482
2834
|
}
|
|
2835
|
+
readOnly(readOnly = true) {
|
|
2836
|
+
this.config.readOnly = readOnly;
|
|
2837
|
+
return this;
|
|
2838
|
+
}
|
|
2839
|
+
disabled(disabled = true) {
|
|
2840
|
+
this.config.disabled = disabled;
|
|
2841
|
+
return this;
|
|
2842
|
+
}
|
|
2483
2843
|
footerActions(actions) {
|
|
2484
2844
|
this.config.footerActions = actions;
|
|
2485
2845
|
return this;
|
|
@@ -2496,25 +2856,107 @@ class BaseModalBuilder {
|
|
|
2496
2856
|
this.config.i18n = labels;
|
|
2497
2857
|
return this;
|
|
2498
2858
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2859
|
+
component(component) {
|
|
2860
|
+
this.config.component = component;
|
|
2861
|
+
return this;
|
|
2501
2862
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
constructor() {
|
|
2506
|
-
super({
|
|
2507
|
-
kind: ModalKind.WIZARD,
|
|
2508
|
-
steps: [],
|
|
2509
|
-
});
|
|
2863
|
+
template(template) {
|
|
2864
|
+
this.config.template = template;
|
|
2865
|
+
return this;
|
|
2510
2866
|
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2867
|
+
inputs(inputs) {
|
|
2868
|
+
this.config.inputs = inputs;
|
|
2869
|
+
return this;
|
|
2870
|
+
}
|
|
2871
|
+
animation(animation) {
|
|
2872
|
+
this.config.animation = animation;
|
|
2873
|
+
return this;
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Add a custom body/content to the modal.
|
|
2877
|
+
*/
|
|
2878
|
+
body(body) {
|
|
2879
|
+
return this.layoutBuilder.body(body);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Add a field.
|
|
2883
|
+
*/
|
|
2884
|
+
field(field) {
|
|
2885
|
+
return this.layoutBuilder.field(field);
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Add a field with a fluent validation builder.
|
|
2889
|
+
*/
|
|
2890
|
+
fieldWithValidators(field) {
|
|
2891
|
+
return this.layoutBuilder.fieldWithValidators(field);
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Start a new row.
|
|
2895
|
+
*/
|
|
2896
|
+
row(columns = 2) {
|
|
2897
|
+
return this.layoutBuilder.row(columns);
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Add a field to the current row.
|
|
2901
|
+
*/
|
|
2902
|
+
addToRow(field, span = 1) {
|
|
2903
|
+
return this.layoutBuilder.addToRow(field, span);
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Declarative way to add a row.
|
|
2907
|
+
*/
|
|
2908
|
+
addRow(columns, buildFn) {
|
|
2909
|
+
return this.layoutBuilder.addRow(columns, buildFn);
|
|
2910
|
+
}
|
|
2911
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2912
|
+
return this.layoutBuilder.fieldGroup(arg1, arg2, arg3);
|
|
2913
|
+
}
|
|
2914
|
+
build() {
|
|
2915
|
+
this.layoutBuilder.flushCurrentRow();
|
|
2916
|
+
return Object.freeze({ ...this.config });
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
class WizardModalBuilder extends BaseModalBuilder {
|
|
2921
|
+
constructor() {
|
|
2922
|
+
super({
|
|
2923
|
+
kind: ModalKind.WIZARD,
|
|
2924
|
+
steps: [],
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Set initial values for the entire wizard.
|
|
2929
|
+
* Note: This will be merged with individual step initial values.
|
|
2930
|
+
*/
|
|
2931
|
+
initialValue(value) {
|
|
2932
|
+
this.config.initialValue = value;
|
|
2933
|
+
return this;
|
|
2934
|
+
}
|
|
2935
|
+
body(body) {
|
|
2936
|
+
return super.body(body);
|
|
2937
|
+
}
|
|
2938
|
+
field(field) {
|
|
2939
|
+
return super.field(field);
|
|
2940
|
+
}
|
|
2941
|
+
row(columns = 2) {
|
|
2942
|
+
return super.row(columns);
|
|
2943
|
+
}
|
|
2944
|
+
addToRow(field, span = 1) {
|
|
2945
|
+
return super.addToRow(field, span);
|
|
2946
|
+
}
|
|
2947
|
+
addRow(columns, buildFn) {
|
|
2948
|
+
return super.addRow(columns, buildFn);
|
|
2949
|
+
}
|
|
2950
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
2951
|
+
return super.fieldGroup(arg1, arg2, arg3);
|
|
2952
|
+
}
|
|
2953
|
+
step(step) {
|
|
2954
|
+
if (typeof step === 'function') {
|
|
2955
|
+
const builder = new StepBuilder('', '');
|
|
2956
|
+
step(builder);
|
|
2957
|
+
const builtStep = builder.build();
|
|
2958
|
+
this.config.steps.push(builtStep);
|
|
2959
|
+
}
|
|
2518
2960
|
else {
|
|
2519
2961
|
this.config.steps.push(step);
|
|
2520
2962
|
}
|
|
@@ -2524,7 +2966,8 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2524
2966
|
const stepId = id || `step-${this.config.steps.length}`;
|
|
2525
2967
|
const builder = new StepBuilder(stepId, title);
|
|
2526
2968
|
buildFn(builder);
|
|
2527
|
-
|
|
2969
|
+
const builtStep = builder.build();
|
|
2970
|
+
this.config.steps.push(builtStep);
|
|
2528
2971
|
return this;
|
|
2529
2972
|
}
|
|
2530
2973
|
startAt(stepId) {
|
|
@@ -2550,8 +2993,6 @@ class WizardModalBuilder extends BaseModalBuilder {
|
|
|
2550
2993
|
}
|
|
2551
2994
|
|
|
2552
2995
|
class FormModalBuilder extends BaseModalBuilder {
|
|
2553
|
-
currentRow = [];
|
|
2554
|
-
currentRowColumns = 1;
|
|
2555
2996
|
constructor() {
|
|
2556
2997
|
super({
|
|
2557
2998
|
kind: ModalKind.FORM,
|
|
@@ -2559,64 +3000,27 @@ class FormModalBuilder extends BaseModalBuilder {
|
|
|
2559
3000
|
rows: [],
|
|
2560
3001
|
});
|
|
2561
3002
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
*/
|
|
3003
|
+
body(body) {
|
|
3004
|
+
return super.body(body);
|
|
3005
|
+
}
|
|
2566
3006
|
field(field) {
|
|
2567
|
-
|
|
2568
|
-
this.config.fields.push(field);
|
|
2569
|
-
this.config.rows.push({
|
|
2570
|
-
columns: 1,
|
|
2571
|
-
fields: [{ field, span: 1 }],
|
|
2572
|
-
});
|
|
2573
|
-
return this;
|
|
3007
|
+
return super.field(field);
|
|
2574
3008
|
}
|
|
2575
|
-
/**
|
|
2576
|
-
* Start a new row with the specified number of columns.
|
|
2577
|
-
* All subsequent `addToRow()` calls will add fields to this row
|
|
2578
|
-
* until the next `row()` or `field()` call.
|
|
2579
|
-
*
|
|
2580
|
-
* @example
|
|
2581
|
-
* .row(2)
|
|
2582
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'firstName', label: 'First Name' })
|
|
2583
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'lastName', label: 'Last Name' })
|
|
2584
|
-
* .row(3)
|
|
2585
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'city', label: 'City' }, 2)
|
|
2586
|
-
* .addToRow({ kind: FieldKind.TEXT, key: 'zip', label: 'ZIP' })
|
|
2587
|
-
*/
|
|
2588
3009
|
row(columns = 2) {
|
|
2589
|
-
|
|
2590
|
-
this.currentRowColumns = columns;
|
|
2591
|
-
return this;
|
|
3010
|
+
return super.row(columns);
|
|
2592
3011
|
}
|
|
2593
|
-
/**
|
|
2594
|
-
* Add a field to the current row started by `row()`.
|
|
2595
|
-
* @param field - The field configuration
|
|
2596
|
-
* @param span - How many columns this field should span (default: 1)
|
|
2597
|
-
*/
|
|
2598
3012
|
addToRow(field, span = 1) {
|
|
2599
|
-
|
|
2600
|
-
this.currentRow.push({ field, span });
|
|
2601
|
-
return this;
|
|
3013
|
+
return super.addToRow(field, span);
|
|
2602
3014
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
this.config.rows.push({
|
|
2606
|
-
columns: this.currentRowColumns,
|
|
2607
|
-
fields: [...this.currentRow],
|
|
2608
|
-
});
|
|
2609
|
-
this.currentRow = [];
|
|
2610
|
-
this.currentRowColumns = 1;
|
|
2611
|
-
}
|
|
3015
|
+
addRow(columns, buildFn) {
|
|
3016
|
+
return super.addRow(columns, buildFn);
|
|
2612
3017
|
}
|
|
2613
3018
|
layout(mode) {
|
|
2614
3019
|
this.config.layout = mode;
|
|
2615
3020
|
return this;
|
|
2616
3021
|
}
|
|
2617
3022
|
initialValue(value) {
|
|
2618
|
-
this.
|
|
2619
|
-
return this;
|
|
3023
|
+
return this.layoutBuilder.initialValue(value);
|
|
2620
3024
|
}
|
|
2621
3025
|
submitMode(mode) {
|
|
2622
3026
|
this.config.submitMode = mode;
|
|
@@ -2626,45 +3030,16 @@ class FormModalBuilder extends BaseModalBuilder {
|
|
|
2626
3030
|
this.config.onComplete = handler;
|
|
2627
3031
|
return this;
|
|
2628
3032
|
}
|
|
2629
|
-
/**
|
|
2630
|
-
* Add form-level validators for cross-field validation.
|
|
2631
|
-
* These receive the entire form value and return an error map or null.
|
|
2632
|
-
*/
|
|
2633
3033
|
formValidators(validators) {
|
|
2634
|
-
this.
|
|
2635
|
-
return this;
|
|
3034
|
+
return this.layoutBuilder.formValidators(validators);
|
|
2636
3035
|
}
|
|
2637
|
-
/**
|
|
2638
|
-
* Add Angular FormGroup-level validators.
|
|
2639
|
-
* These are standard Angular ValidatorFn applied to the FormGroup itself.
|
|
2640
|
-
*/
|
|
2641
3036
|
groupValidators(validators) {
|
|
2642
|
-
this.
|
|
2643
|
-
return this;
|
|
3037
|
+
return this.layoutBuilder.groupValidators(validators);
|
|
2644
3038
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
* Groups visually separate fields with a title and optional description.
|
|
2648
|
-
*/
|
|
2649
|
-
fieldGroup(group) {
|
|
2650
|
-
this.flushCurrentRow();
|
|
2651
|
-
if (!this.config.fieldGroups) {
|
|
2652
|
-
this.config.fieldGroups = [];
|
|
2653
|
-
}
|
|
2654
|
-
// Also add group fields to the flat fields array for form control creation
|
|
2655
|
-
group.fields.forEach(f => this.config.fields.push(f));
|
|
2656
|
-
// Build rows for the group if not provided
|
|
2657
|
-
if (!group.rows) {
|
|
2658
|
-
group.rows = group.fields.map(f => ({
|
|
2659
|
-
columns: 1,
|
|
2660
|
-
fields: [{ field: f, span: 1 }],
|
|
2661
|
-
}));
|
|
2662
|
-
}
|
|
2663
|
-
this.config.fieldGroups.push(group);
|
|
2664
|
-
return this;
|
|
3039
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
3040
|
+
return super.fieldGroup(arg1, arg2, arg3);
|
|
2665
3041
|
}
|
|
2666
3042
|
build() {
|
|
2667
|
-
this.flushCurrentRow();
|
|
2668
3043
|
return super.build();
|
|
2669
3044
|
}
|
|
2670
3045
|
}
|
|
@@ -2674,6 +3049,8 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
|
|
|
2674
3049
|
super({
|
|
2675
3050
|
kind: ModalKind.CONFIRMATION,
|
|
2676
3051
|
message: '',
|
|
3052
|
+
fields: [],
|
|
3053
|
+
rows: [],
|
|
2677
3054
|
});
|
|
2678
3055
|
}
|
|
2679
3056
|
message(text) {
|
|
@@ -2692,6 +3069,36 @@ class ConfirmationModalBuilder extends BaseModalBuilder {
|
|
|
2692
3069
|
this.config.cancel = action;
|
|
2693
3070
|
return this;
|
|
2694
3071
|
}
|
|
3072
|
+
body(body) {
|
|
3073
|
+
return super.body(body);
|
|
3074
|
+
}
|
|
3075
|
+
field(field) {
|
|
3076
|
+
return super.field(field);
|
|
3077
|
+
}
|
|
3078
|
+
row(columns = 2) {
|
|
3079
|
+
return super.row(columns);
|
|
3080
|
+
}
|
|
3081
|
+
addToRow(field, span = 1) {
|
|
3082
|
+
return super.addToRow(field, span);
|
|
3083
|
+
}
|
|
3084
|
+
addRow(columns, buildFn) {
|
|
3085
|
+
return super.addRow(columns, buildFn);
|
|
3086
|
+
}
|
|
3087
|
+
fieldGroup(arg1, arg2, arg3) {
|
|
3088
|
+
return super.fieldGroup(arg1, arg2, arg3);
|
|
3089
|
+
}
|
|
3090
|
+
initialValue(value) {
|
|
3091
|
+
return this.layoutBuilder.initialValue(value);
|
|
3092
|
+
}
|
|
3093
|
+
formValidators(validators) {
|
|
3094
|
+
return this.layoutBuilder.formValidators(validators);
|
|
3095
|
+
}
|
|
3096
|
+
groupValidators(validators) {
|
|
3097
|
+
return this.layoutBuilder.groupValidators(validators);
|
|
3098
|
+
}
|
|
3099
|
+
build() {
|
|
3100
|
+
return super.build();
|
|
3101
|
+
}
|
|
2695
3102
|
}
|
|
2696
3103
|
|
|
2697
3104
|
class CustomModalBuilder extends BaseModalBuilder {
|
|
@@ -2700,18 +3107,6 @@ class CustomModalBuilder extends BaseModalBuilder {
|
|
|
2700
3107
|
kind: ModalKind.CUSTOM,
|
|
2701
3108
|
});
|
|
2702
3109
|
}
|
|
2703
|
-
component(component) {
|
|
2704
|
-
this.config.component = component;
|
|
2705
|
-
return this;
|
|
2706
|
-
}
|
|
2707
|
-
template(template) {
|
|
2708
|
-
this.config.template = template;
|
|
2709
|
-
return this;
|
|
2710
|
-
}
|
|
2711
|
-
inputs(inputs) {
|
|
2712
|
-
this.config.inputs = inputs;
|
|
2713
|
-
return this;
|
|
2714
|
-
}
|
|
2715
3110
|
onComplete(handler) {
|
|
2716
3111
|
this.config.onComplete = handler;
|
|
2717
3112
|
return this;
|
|
@@ -2769,6 +3164,9 @@ class MnModalRef {
|
|
|
2769
3164
|
// Trigger change detection on the shell component
|
|
2770
3165
|
this.componentRef.changeDetectorRef.detectChanges();
|
|
2771
3166
|
}
|
|
3167
|
+
get component() {
|
|
3168
|
+
return this.componentRef.instance;
|
|
3169
|
+
}
|
|
2772
3170
|
destroy() {
|
|
2773
3171
|
this.componentRef.destroy();
|
|
2774
3172
|
}
|
|
@@ -3091,11 +3489,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3091
3489
|
type: Output
|
|
3092
3490
|
}] } });
|
|
3093
3491
|
|
|
3492
|
+
class MnCustomBodyHostComponent {
|
|
3493
|
+
config;
|
|
3494
|
+
modalRef;
|
|
3495
|
+
container;
|
|
3496
|
+
componentRef;
|
|
3497
|
+
ngOnInit() {
|
|
3498
|
+
setTimeout(() => this.loadContent(), 0);
|
|
3499
|
+
}
|
|
3500
|
+
loadContent() {
|
|
3501
|
+
if (!this.container)
|
|
3502
|
+
return;
|
|
3503
|
+
this.container.clear();
|
|
3504
|
+
if (this.config.component) {
|
|
3505
|
+
this.attachComponent(this.config.component);
|
|
3506
|
+
}
|
|
3507
|
+
else if (this.config.template) {
|
|
3508
|
+
this.attachTemplate(this.config.template);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
attachComponent(component) {
|
|
3512
|
+
this.componentRef = this.container.createComponent(component);
|
|
3513
|
+
// Pass inputs to the component
|
|
3514
|
+
if (this.config.inputs) {
|
|
3515
|
+
Object.entries(this.config.inputs).forEach(([key, value]) => {
|
|
3516
|
+
this.componentRef.instance[key] = value;
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
// Pass modalRef if the component has a modalRef property
|
|
3520
|
+
const instance = this.componentRef.instance;
|
|
3521
|
+
if (instance && 'modalRef' in instance) {
|
|
3522
|
+
instance.modalRef = this.modalRef;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
attachTemplate(template) {
|
|
3526
|
+
this.container.createEmbeddedView(template, {
|
|
3527
|
+
$implicit: this.modalRef,
|
|
3528
|
+
modalRef: this.modalRef,
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3532
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnCustomBodyHostComponent, isStandalone: true, selector: "mn-custom-body-host", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: '<ng-container #container></ng-container>', isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
3533
|
+
}
|
|
3534
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCustomBodyHostComponent, decorators: [{
|
|
3535
|
+
type: Component,
|
|
3536
|
+
args: [{
|
|
3537
|
+
selector: 'mn-custom-body-host',
|
|
3538
|
+
standalone: true,
|
|
3539
|
+
imports: [CommonModule],
|
|
3540
|
+
template: '<ng-container #container></ng-container>',
|
|
3541
|
+
}]
|
|
3542
|
+
}], propDecorators: { config: [{
|
|
3543
|
+
type: Input
|
|
3544
|
+
}], modalRef: [{
|
|
3545
|
+
type: Input
|
|
3546
|
+
}], container: [{
|
|
3547
|
+
type: ViewChild,
|
|
3548
|
+
args: ['container', { read: ViewContainerRef }]
|
|
3549
|
+
}] } });
|
|
3550
|
+
|
|
3094
3551
|
class MnFormBodyComponent {
|
|
3095
3552
|
fb;
|
|
3096
3553
|
config;
|
|
3097
3554
|
modalRef;
|
|
3098
3555
|
hideFooter = false;
|
|
3556
|
+
hideCustomBody = false;
|
|
3557
|
+
formStatusChange = new EventEmitter();
|
|
3558
|
+
inputFields;
|
|
3559
|
+
textareas;
|
|
3099
3560
|
form;
|
|
3100
3561
|
rows = [];
|
|
3101
3562
|
fieldGroups = [];
|
|
@@ -3154,6 +3615,44 @@ class MnFormBodyComponent {
|
|
|
3154
3615
|
this.initializeDataSources();
|
|
3155
3616
|
this.initializeTableFields();
|
|
3156
3617
|
this.subscribeToValueChanges();
|
|
3618
|
+
if (this.config.disabled || this.config.readOnly) {
|
|
3619
|
+
this.form.disable();
|
|
3620
|
+
}
|
|
3621
|
+
// Emit initial status
|
|
3622
|
+
setTimeout(() => {
|
|
3623
|
+
this.formStatusChange.emit(this.form.status);
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
ngAfterViewInit() {
|
|
3627
|
+
// Small delay to ensure children are fully rendered and MnModalShell hasn't just stolen focus
|
|
3628
|
+
setTimeout(() => {
|
|
3629
|
+
this.applyAutoFocus();
|
|
3630
|
+
}, 100);
|
|
3631
|
+
}
|
|
3632
|
+
applyAutoFocus() {
|
|
3633
|
+
const autoFocusField = this.config.fields.find(f => f.autoFocus);
|
|
3634
|
+
if (!autoFocusField)
|
|
3635
|
+
return;
|
|
3636
|
+
const key = autoFocusField.key;
|
|
3637
|
+
// Small delay to ensure browser is ready to focus
|
|
3638
|
+
setTimeout(() => {
|
|
3639
|
+
// Try finding in MnInputField components
|
|
3640
|
+
const inputField = this.inputFields?.find(f => f.props.id === key);
|
|
3641
|
+
if (inputField) {
|
|
3642
|
+
inputField.focus();
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
// Try finding in MnTextarea components
|
|
3646
|
+
const textarea = this.textareas?.find(f => f.props?.id === key);
|
|
3647
|
+
if (textarea && typeof textarea.focus === 'function') {
|
|
3648
|
+
textarea.focus();
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
// Fallback to native element if possible
|
|
3652
|
+
const el = document.getElementById(key);
|
|
3653
|
+
if (el)
|
|
3654
|
+
el.focus();
|
|
3655
|
+
}, 50);
|
|
3157
3656
|
}
|
|
3158
3657
|
ngOnDestroy() {
|
|
3159
3658
|
this.valueChangesSubscription?.unsubscribe();
|
|
@@ -3169,7 +3668,15 @@ class MnFormBodyComponent {
|
|
|
3169
3668
|
}
|
|
3170
3669
|
const validators = fieldConfig.validators || [];
|
|
3171
3670
|
const asyncValidators = fieldConfig.asyncValidators || [];
|
|
3172
|
-
|
|
3671
|
+
const updateOn = fieldConfig.updateOn || 'change';
|
|
3672
|
+
formControls[field.key] = [
|
|
3673
|
+
initialValue,
|
|
3674
|
+
{
|
|
3675
|
+
validators,
|
|
3676
|
+
asyncValidators,
|
|
3677
|
+
updateOn
|
|
3678
|
+
}
|
|
3679
|
+
];
|
|
3173
3680
|
});
|
|
3174
3681
|
this.form = this.fb.group(formControls);
|
|
3175
3682
|
// Apply Angular FormGroup-level validators
|
|
@@ -3187,10 +3694,10 @@ class MnFormBodyComponent {
|
|
|
3187
3694
|
});
|
|
3188
3695
|
}
|
|
3189
3696
|
isFieldReadOnly(field) {
|
|
3190
|
-
return field.readOnly === true;
|
|
3697
|
+
return this.config.readOnly === true || field.readOnly === true;
|
|
3191
3698
|
}
|
|
3192
3699
|
isFieldDisabled(field) {
|
|
3193
|
-
return field.disabled === true;
|
|
3700
|
+
return this.config.disabled === true || field.disabled === true;
|
|
3194
3701
|
}
|
|
3195
3702
|
/** Track which field groups are currently visible */
|
|
3196
3703
|
groupVisibility = {};
|
|
@@ -3198,12 +3705,25 @@ class MnFormBodyComponent {
|
|
|
3198
3705
|
if (this.config.rows && this.config.rows.length > 0) {
|
|
3199
3706
|
this.rows = this.config.rows;
|
|
3200
3707
|
}
|
|
3201
|
-
else
|
|
3202
|
-
//
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3708
|
+
else {
|
|
3709
|
+
// Create rows for fields that are not in any row or group
|
|
3710
|
+
const fieldsInGroups = new Set();
|
|
3711
|
+
(this.config.fieldGroups || []).forEach(g => {
|
|
3712
|
+
(g.rows || []).forEach(r => r.fields.forEach(f => fieldsInGroups.add(f.field.key)));
|
|
3713
|
+
// Also check if group has flat fields list (compatibility)
|
|
3714
|
+
if (g.fields) {
|
|
3715
|
+
g.fields.forEach((f) => fieldsInGroups.add(f.key));
|
|
3716
|
+
}
|
|
3717
|
+
});
|
|
3718
|
+
const fieldsInRows = new Set();
|
|
3719
|
+
(this.config.rows || []).forEach(r => r.fields.forEach(f => fieldsInRows.add(f.field.key)));
|
|
3720
|
+
const standaloneFields = this.config.fields.filter(f => !fieldsInGroups.has(f.key) && !fieldsInRows.has(f.key));
|
|
3721
|
+
if (standaloneFields.length > 0) {
|
|
3722
|
+
this.rows = standaloneFields.map(field => ({
|
|
3723
|
+
columns: 1,
|
|
3724
|
+
fields: [{ field, span: 1 }],
|
|
3725
|
+
}));
|
|
3726
|
+
}
|
|
3207
3727
|
}
|
|
3208
3728
|
// Build field groups
|
|
3209
3729
|
this.fieldGroups = this.config.fieldGroups || [];
|
|
@@ -3482,6 +4002,8 @@ class MnFormBodyComponent {
|
|
|
3482
4002
|
this.runFormValidators();
|
|
3483
4003
|
// Reload data sources that depend on changed fields
|
|
3484
4004
|
this.reloadDependentDataSources(formValue);
|
|
4005
|
+
// Emit status change
|
|
4006
|
+
this.formStatusChange.emit(this.form.status);
|
|
3485
4007
|
});
|
|
3486
4008
|
}
|
|
3487
4009
|
previousFormValue = {};
|
|
@@ -3601,17 +4123,27 @@ class MnFormBodyComponent {
|
|
|
3601
4123
|
}
|
|
3602
4124
|
}
|
|
3603
4125
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, deps: [{ token: i1$2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component });
|
|
3604
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter" }, ngImport: i0, template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-xl text-base text-gray-900 bg-white transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key))\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label\n })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step\n })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical'\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections\n })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-gray-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n\n <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"fieldGroups.length === 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", inputs: ["dataSource"], outputs: ["sortChange", "selectionChange", "rowClick"] }] });
|
|
4126
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "<mn-custom-body-host\n *ngIf=\"(config.component || config.template) && !hideCustomBody\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n></mn-custom-body-host>\n\n<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-xl text-base text-gray-900 bg-white transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key)) || isFieldDisabled(rowField.field)\"\n [attr.disabled]=\"isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field) ? '' : null\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-gray-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n\n <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"rows.length > 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", inputs: ["dataSource"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
|
|
3605
4127
|
}
|
|
3606
4128
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
|
|
3607
4129
|
type: Component,
|
|
3608
|
-
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnCustomFieldHostDirective, MnTable], template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-xl text-base text-gray-900 bg-white transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key))\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label\n })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step\n })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical'\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections\n })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-gray-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n\n <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"fieldGroups.length === 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"] }]
|
|
4130
|
+
args: [{ selector: 'mn-form-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnInputField, MnCheckbox, MnDatetime, MnMultiSelect, MnTextarea, MnCustomFieldHostDirective, MnTable, MnCustomBodyHostComponent], template: "<mn-custom-body-host\n *ngIf=\"(config.component || config.template) && !hideCustomBody\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n></mn-custom-body-host>\n\n<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"flex flex-col gap-6\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n <ng-container [ngSwitch]=\"rowField.field.kind\">\n <div *ngSwitchCase=\"FieldKind.TEXT\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.NUMBER\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.PASSWORD\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SELECT\" class=\"flex flex-col\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\" [for]=\"asKey(rowField.field.key)\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"relative\">\n <select\n [id]=\"asKey(rowField.field.key)\"\n [formControlName]=\"asKey(rowField.field.key)\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-xl text-base text-gray-900 bg-white transition-all appearance-none pr-8 focus:outline-none focus:border-blue-500 focus:ring-[3px] focus:ring-blue-500/10 select-arrow\"\n [class.opacity-50]=\"isFieldLoading(asKey(rowField.field.key)) || isFieldDisabled(rowField.field)\"\n [attr.disabled]=\"isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field) ? '' : null\"\n >\n <option value=\"\" disabled selected hidden>\n {{ isFieldLoading(asKey(rowField.field.key)) ? labels.loading : labels.selectPlaceholder }}\n </option>\n <option\n *ngFor=\"let option of getFieldOptions(rowField.field)\"\n [value]=\"option.value\"\n [disabled]=\"option.state === 'disabled'\"\n >\n {{ option.label }}\n </option>\n </select>\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"absolute right-8 top-1/2 -translate-y-1/2\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CHECKBOX\">\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATE\">\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.DATETIME\">\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.TEXTAREA\">\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT\">\n <div *ngIf=\"isFieldLoading(asKey(rowField.field.key))\" class=\"flex items-center gap-2 py-2 text-sm text-gray-500\">\n <div class=\"w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin\"></div>\n Loading options...\n </div>\n <mn-lib-multi-select\n *ngIf=\"!isFieldLoading(asKey(rowField.field.key))\"\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n <div *ngIf=\"getFieldError(asKey(rowField.field.key))\" class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.MULTI_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SINGLE_SELECT_TABLE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.COLOR\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n class=\"w-10 h-10 rounded-lg border border-gray-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-gray-600 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n <div *ngIf=\"asField(rowField.field).swatches\" class=\"flex gap-1.5 mt-1\">\n <button\n *ngFor=\"let swatch of asField(rowField.field).swatches\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-gray-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n ></button>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.RATING\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-1\">\n <button\n *ngFor=\"let star of getRatingRange(rowField.field)\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-gray-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n <span class=\"text-sm text-gray-500 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.SLIDER\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n class=\"flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n <span *ngIf=\"asField(rowField.field).showValue !== false\" class=\"text-sm text-gray-600 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n </div>\n <div class=\"flex justify-between text-xs text-gray-400 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n This field is required\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.FILE\" class=\"flex flex-col gap-1\">\n <label class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-gray-700\">\n {{ asField(rowField.field).label }}\n <span *ngIf=\"hasRequiredValidator(rowField.field)\" class=\"text-red-500\">*</span>\n </label>\n <div\n class=\"relative border-2 border-dashed border-gray-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-gray-500\">{{ labels.fileUploadPrompt }}</p>\n <p *ngIf=\"asField(rowField.field).accept\" class=\"text-xs text-gray-400\">Accepted: {{ asField(rowField.field).accept }}</p>\n <p *ngIf=\"asField(rowField.field).maxSize\" class=\"text-xs text-gray-400\">Max size: {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n </div>\n </div>\n <!-- Selected files list -->\n <div *ngIf=\"getSelectedFiles(asKey(rowField.field.key)).length > 0\" class=\"flex flex-col gap-1 mt-2\">\n <div *ngFor=\"let file of getSelectedFiles(asKey(rowField.field.key)); let i = index\" class=\"flex items-center justify-between px-3 py-1.5 bg-gray-50 rounded-lg text-sm\">\n <span class=\"text-gray-700 truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button type=\"button\" class=\"text-gray-400 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n </div>\n <div\n *ngIf=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFileError(rowField.field) }}\n </div>\n </div>\n\n <div *ngSwitchCase=\"FieldKind.CUSTOM\">\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n\n <div *ngSwitchDefault>\n </div>\n </ng-container>\n\n <!-- Show cross-field error below any field that has one -->\n <div\n *ngIf=\"getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT\"\n class=\"text-red-500 text-xs pl-2 pt-1\"\n >\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n <div *ngIf=\"fieldGroups.length > 0\" class=\"flex flex-col gap-6\">\n <div *ngFor=\"let group of fieldGroups\" class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-gray-200 pb-2\">\n <h3 class=\"text-base font-semibold text-gray-900\">{{ group.title }}</h3>\n <p *ngIf=\"group.description\" class=\"text-sm text-gray-500 mt-0.5\">{{ group.description }}</p>\n </div>\n <div *ngFor=\"let row of group.rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Standard rows (no groups) -->\n <div class=\"flex flex-col gap-4\" *ngIf=\"rows.length > 0\">\n <div *ngFor=\"let row of rows\" class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n <div *ngFor=\"let rowField of row.fields\" class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n </div>\n </div>\n\n <!-- Form-level errors (not tied to a specific field) -->\n <div *ngIf=\"formErrors['_form']\" class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\" *ngIf=\"!hideFooter\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"] }]
|
|
3609
4131
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }], propDecorators: { config: [{
|
|
3610
4132
|
type: Input
|
|
3611
4133
|
}], modalRef: [{
|
|
3612
4134
|
type: Input
|
|
3613
4135
|
}], hideFooter: [{
|
|
3614
4136
|
type: Input
|
|
4137
|
+
}], hideCustomBody: [{
|
|
4138
|
+
type: Input
|
|
4139
|
+
}], formStatusChange: [{
|
|
4140
|
+
type: Output
|
|
4141
|
+
}], inputFields: [{
|
|
4142
|
+
type: ViewChildren,
|
|
4143
|
+
args: [MnInputField]
|
|
4144
|
+
}], textareas: [{
|
|
4145
|
+
type: ViewChildren,
|
|
4146
|
+
args: [MnTextarea]
|
|
3615
4147
|
}] } });
|
|
3616
4148
|
|
|
3617
4149
|
class MnWizardBodyComponent {
|
|
@@ -3646,6 +4178,11 @@ class MnWizardBodyComponent {
|
|
|
3646
4178
|
// Pre-build form configs for all form-driven steps
|
|
3647
4179
|
for (const step of this.config.steps) {
|
|
3648
4180
|
if (step.fields && step.fields.length > 0) {
|
|
4181
|
+
// Merge top-level initialValue with step-level initialValue
|
|
4182
|
+
const mergedInitialValue = {
|
|
4183
|
+
...(this.config.initialValue || {}),
|
|
4184
|
+
...(step.initialValue || {})
|
|
4185
|
+
};
|
|
3649
4186
|
this.stepFormConfigs[step.id] = {
|
|
3650
4187
|
kind: ModalKind.FORM,
|
|
3651
4188
|
fields: step.fields,
|
|
@@ -3653,7 +4190,9 @@ class MnWizardBodyComponent {
|
|
|
3653
4190
|
fieldGroups: step.fieldGroups,
|
|
3654
4191
|
formValidators: step.formValidators,
|
|
3655
4192
|
groupValidators: step.groupValidators,
|
|
3656
|
-
initialValue:
|
|
4193
|
+
initialValue: mergedInitialValue,
|
|
4194
|
+
readOnly: this.config.readOnly,
|
|
4195
|
+
disabled: this.config.disabled
|
|
3657
4196
|
};
|
|
3658
4197
|
}
|
|
3659
4198
|
}
|
|
@@ -3666,6 +4205,10 @@ class MnWizardBodyComponent {
|
|
|
3666
4205
|
// Subscribe to form bodies list changes to track validity
|
|
3667
4206
|
this.formBodiesSubscription = this.formBodies.changes.subscribe(() => {
|
|
3668
4207
|
this.trackCurrentStepValidity();
|
|
4208
|
+
// Apply autofocus when form bodies change (e.g. step navigation)
|
|
4209
|
+
setTimeout(() => {
|
|
4210
|
+
this.getCurrentFormBody()?.applyAutoFocus();
|
|
4211
|
+
}, 100);
|
|
3669
4212
|
});
|
|
3670
4213
|
// Initial validity check
|
|
3671
4214
|
this.trackCurrentStepValidity();
|
|
@@ -3891,11 +4434,11 @@ class MnWizardBodyComponent {
|
|
|
3891
4434
|
}
|
|
3892
4435
|
}
|
|
3893
4436
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
3894
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\">\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ canGoBack ? labels.back : labels.close }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ isCompleting ? labels.completing : labels.complete }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
4437
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\">\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"block\"\n ></mn-custom-body-host>\n\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n *ngIf=\"!currentStep?.hideBack\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
3895
4438
|
}
|
|
3896
4439
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
|
|
3897
4440
|
type: Component,
|
|
3898
|
-
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent], template: "<div class=\"flex flex-col gap-6\">\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ canGoBack ? labels.back : labels.close }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ isCompleting ? labels.completing : labels.complete }}\n </button>\n </div>\n</div>\n" }]
|
|
4441
|
+
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent], template: "<div class=\"flex flex-col gap-6\">\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"block\"\n ></mn-custom-body-host>\n\n <div class=\"flex gap-2 pb-4 border-b border-gray-200\">\n <div\n *ngFor=\"let step of visibleSteps; let i = index\"\n class=\"flex items-center gap-2 flex-1\"\n [class.active]=\"step.id === currentStepId\"\n [class.complete]=\"visitedStepIds.includes(step.id) && step.id !== currentStepId\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n >\n <div\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n [ngClass]=\"{\n 'bg-blue-500 text-white': step.id === currentStepId,\n 'bg-green-500 text-white': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-gray-200 text-gray-500': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n >{{ i + 1 }}</div>\n <div\n class=\"text-sm\"\n [ngClass]=\"{\n 'text-gray-900 font-semibold': step.id === currentStepId,\n 'text-gray-500': step.id !== currentStepId\n }\"\n >{{ step.title }}</div>\n </div>\n </div>\n\n <div class=\"min-h-48\">\n <ng-container *ngFor=\"let step of config.steps\">\n <div [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-gray-900 mb-4\">{{ step.title }}</h3>\n <div class=\"text-gray-700\">\n <!-- Form step -->\n <mn-form-body\n *ngIf=\"stepFormConfigs[step.id]\"\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n\n <!-- Text body -->\n <div *ngIf=\"!stepFormConfigs[step.id] && isTextBody(step)\">\n {{ step.body }}\n </div>\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n </ng-container>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) -->\n <div *ngIf=\"wizardErrors && (wizardErrors | keyvalue).length > 0\" class=\"flex flex-col gap-1 px-2 py-2 bg-red-50 rounded-md\">\n <div *ngFor=\"let err of wizardErrors | keyvalue\" class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n </div>\n\n <div class=\"flex gap-3 pt-4 border-t border-gray-200\">\n <button\n *ngIf=\"!currentStep?.hideBack\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n *ngIf=\"!isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n\n <button\n *ngIf=\"isLastStep\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n </div>\n</div>\n" }]
|
|
3899
4442
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
|
|
3900
4443
|
type: Input
|
|
3901
4444
|
}], modalRef: [{
|
|
@@ -3906,10 +4449,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3906
4449
|
}] } });
|
|
3907
4450
|
|
|
3908
4451
|
class MnConfirmationBodyComponent {
|
|
4452
|
+
cdr;
|
|
3909
4453
|
config;
|
|
3910
4454
|
modalRef;
|
|
4455
|
+
formBody;
|
|
4456
|
+
confirmButtonStatus = 'VALID';
|
|
4457
|
+
hasFormFields = false;
|
|
4458
|
+
constructor(cdr) {
|
|
4459
|
+
this.cdr = cdr;
|
|
4460
|
+
}
|
|
4461
|
+
ngOnInit() {
|
|
4462
|
+
this.hasFormFields = !!((this.config.fields && this.config.fields.length > 0) ||
|
|
4463
|
+
(this.config.fieldGroups && this.config.fieldGroups.length > 0) ||
|
|
4464
|
+
(this.config.rows && this.config.rows.length > 0));
|
|
4465
|
+
}
|
|
4466
|
+
onFormStatusChange(status) {
|
|
4467
|
+
this.confirmButtonStatus = status;
|
|
4468
|
+
this.cdr.markForCheck();
|
|
4469
|
+
this.cdr.detectChanges();
|
|
4470
|
+
}
|
|
3911
4471
|
async confirm() {
|
|
3912
|
-
|
|
4472
|
+
if (this.hasFormFields && this.formBody?.form.invalid) {
|
|
4473
|
+
this.formBody.form.markAllAsTouched();
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
const result = (this.hasFormFields ? this.formBody?.form.value : true);
|
|
3913
4477
|
if (this.config.confirm?.handler) {
|
|
3914
4478
|
await this.config.confirm.handler.handle(result);
|
|
3915
4479
|
}
|
|
@@ -3964,75 +4528,31 @@ class MnConfirmationBodyComponent {
|
|
|
3964
4528
|
return 'outline';
|
|
3965
4529
|
}
|
|
3966
4530
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
}
|
|
3970
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
|
|
3971
|
-
type: Component,
|
|
3972
|
-
args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton], template: "<div class=\"flex flex-col items-center gap-6 text-center\" [ngClass]=\"toneClass\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle)\n }\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
|
|
3973
|
-
}], propDecorators: { config: [{
|
|
3974
|
-
type: Input
|
|
3975
|
-
}], modalRef: [{
|
|
3976
|
-
type: Input
|
|
3977
|
-
}] } });
|
|
3978
|
-
|
|
3979
|
-
class MnCustomBodyHostComponent {
|
|
3980
|
-
config;
|
|
3981
|
-
modalRef;
|
|
3982
|
-
container;
|
|
3983
|
-
componentRef;
|
|
3984
|
-
ngOnInit() {
|
|
3985
|
-
setTimeout(() => this.loadContent(), 0);
|
|
3986
|
-
}
|
|
3987
|
-
loadContent() {
|
|
3988
|
-
if (!this.container)
|
|
3989
|
-
return;
|
|
3990
|
-
this.container.clear();
|
|
3991
|
-
if (this.config.component) {
|
|
3992
|
-
this.attachComponent(this.config.component);
|
|
3993
|
-
}
|
|
3994
|
-
else if (this.config.template) {
|
|
3995
|
-
this.attachTemplate(this.config.template);
|
|
3996
|
-
}
|
|
4531
|
+
asAny(val) {
|
|
4532
|
+
return val;
|
|
3997
4533
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
if (this.config.inputs) {
|
|
4002
|
-
Object.entries(this.config.inputs).forEach(([key, value]) => {
|
|
4003
|
-
this.componentRef.instance[key] = value;
|
|
4004
|
-
});
|
|
4005
|
-
}
|
|
4006
|
-
// Pass modalRef if the component has a modalRef property
|
|
4007
|
-
const instance = this.componentRef.instance;
|
|
4008
|
-
if (instance && 'modalRef' in instance) {
|
|
4009
|
-
instance.modalRef = this.modalRef;
|
|
4534
|
+
get isConfirmDisabled() {
|
|
4535
|
+
if (this.hasFormFields) {
|
|
4536
|
+
return this.confirmButtonStatus !== 'VALID';
|
|
4010
4537
|
}
|
|
4538
|
+
return false;
|
|
4011
4539
|
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
$implicit: this.modalRef,
|
|
4015
|
-
modalRef: this.modalRef,
|
|
4016
|
-
});
|
|
4540
|
+
asField(field) {
|
|
4541
|
+
return field;
|
|
4017
4542
|
}
|
|
4018
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type:
|
|
4019
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type:
|
|
4543
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
4544
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnConfirmationBodyComponent, isStandalone: true, selector: "mn-confirmation-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBody", first: true, predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\" *ngIf=\"config.message\">\n {{ config.message }}\n </div>\n </div>\n\n <!-- Form Fields / Rows -->\n <mn-form-body\n *ngIf=\"hasFormFields\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "ngmodule", type: ReactiveFormsModule }] });
|
|
4020
4545
|
}
|
|
4021
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type:
|
|
4546
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
|
|
4022
4547
|
type: Component,
|
|
4023
|
-
args: [{
|
|
4024
|
-
|
|
4025
|
-
standalone: true,
|
|
4026
|
-
imports: [CommonModule],
|
|
4027
|
-
template: '<ng-container #container></ng-container>',
|
|
4028
|
-
}]
|
|
4029
|
-
}], propDecorators: { config: [{
|
|
4548
|
+
args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, ReactiveFormsModule], template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n <mn-custom-body-host\n *ngIf=\"config.component || config.template\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n <div *ngIf=\"config.tone === 'warning' || config.tone === 'danger'\"\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n <svg\n *ngIf=\"config.tone === 'warning'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z\"\n />\n </svg>\n <svg\n *ngIf=\"config.tone === 'danger'\"\n class=\"w-6 h-6\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke-width=\"1.5\"\n stroke=\"currentColor\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z\"\n />\n </svg>\n </div>\n\n <div class=\"text-base text-gray-700 leading-relaxed max-w-[28rem]\" *ngIf=\"config.message\">\n {{ config.message }}\n </div>\n </div>\n\n <!-- Form Fields / Rows -->\n <mn-form-body\n *ngIf=\"hasFormFields\"\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n\n <div class=\"flex gap-3 w-full\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
|
|
4549
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
|
|
4030
4550
|
type: Input
|
|
4031
4551
|
}], modalRef: [{
|
|
4032
4552
|
type: Input
|
|
4033
|
-
}],
|
|
4553
|
+
}], formBody: [{
|
|
4034
4554
|
type: ViewChild,
|
|
4035
|
-
args: [
|
|
4555
|
+
args: [MnFormBodyComponent]
|
|
4036
4556
|
}] } });
|
|
4037
4557
|
|
|
4038
4558
|
class MnModalShellComponent {
|
|
@@ -4040,6 +4560,7 @@ class MnModalShellComponent {
|
|
|
4040
4560
|
config;
|
|
4041
4561
|
modalRef;
|
|
4042
4562
|
isClosing = false;
|
|
4563
|
+
isStacked = false;
|
|
4043
4564
|
ModalKind = ModalKind;
|
|
4044
4565
|
previouslyFocusedElement = null;
|
|
4045
4566
|
focusTrapListener = null;
|
|
@@ -4116,7 +4637,12 @@ class MnModalShellComponent {
|
|
|
4116
4637
|
get hostClasses() {
|
|
4117
4638
|
const size = this.config.size || ModalSize.MD;
|
|
4118
4639
|
const closing = this.isClosing ? ' closing' : '';
|
|
4119
|
-
|
|
4640
|
+
const animType = typeof this.config.animation === 'string'
|
|
4641
|
+
? this.config.animation
|
|
4642
|
+
: this.config.animation?.type || 'slide';
|
|
4643
|
+
const animation = ` anim-${animType}`;
|
|
4644
|
+
const stacked = this.isStacked ? ' is-stacked' : '';
|
|
4645
|
+
return `modal-shell modal-${size}${closing}${animation}${stacked}`;
|
|
4120
4646
|
}
|
|
4121
4647
|
startClosing() {
|
|
4122
4648
|
this.isClosing = true;
|
|
@@ -4167,6 +4693,17 @@ class MnModalShellComponent {
|
|
|
4167
4693
|
get showCloseButton() {
|
|
4168
4694
|
return this.config.closeMode !== CloseMode.DISABLED;
|
|
4169
4695
|
}
|
|
4696
|
+
get animationClass() {
|
|
4697
|
+
const animType = typeof this.config.animation === 'string'
|
|
4698
|
+
? this.config.animation
|
|
4699
|
+
: this.config.animation?.type || 'slide';
|
|
4700
|
+
switch (animType) {
|
|
4701
|
+
case 'fade': return 'animate-[fadeIn_0.2s_ease-in-out]';
|
|
4702
|
+
case 'zoom': return 'animate-[zoomIn_0.2s_ease-in-out]';
|
|
4703
|
+
case 'slide':
|
|
4704
|
+
default: return 'animate-[slideIn_0.2s_ease-in-out]';
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4170
4707
|
// =========================
|
|
4171
4708
|
// Footer Actions
|
|
4172
4709
|
// =========================
|
|
@@ -4248,7 +4785,7 @@ class MnModalShellComponent {
|
|
|
4248
4785
|
}
|
|
4249
4786
|
}
|
|
4250
4787
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
4251
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col
|
|
4788
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [ngClass]=\"[containerSizeClass, animationClass]\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n <div class=\"flex items-center justify-between p-6 border-b border-gray-200\">\n <div class=\"flex flex-col gap-0.5\">\n <h2 class=\"m-0 text-xl font-semibold text-gray-900\" *ngIf=\"config.title\" id=\"mn-modal-title\">{{ config.title }}</h2>\n <p class=\"m-0 text-sm text-gray-500 font-normal\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n </div>\n <button\n *ngIf=\"showCloseButton\"\n class=\"bg-transparent border-none text-2xl cursor-pointer text-gray-500 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-gray-100 hover:text-gray-900\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n </div>\n <p class=\"m-0 px-6 text-sm text-gray-500 leading-relaxed\" *ngIf=\"config.description\" id=\"mn-modal-description\">{{ config.description }}</p>\n\n <div class=\"flex-1 overflow-y-auto p-6\">\n <mn-wizard-body\n *ngIf=\"config.kind === ModalKind.WIZARD\"\n [config]=\"asWizard(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-wizard-body>\n\n <mn-form-body\n *ngIf=\"config.kind === ModalKind.FORM\"\n [config]=\"asForm(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-form-body>\n\n <mn-confirmation-body\n *ngIf=\"config.kind === ModalKind.CONFIRMATION\"\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-confirmation-body>\n\n <mn-custom-body-host\n *ngIf=\"config.kind === ModalKind.CUSTOM\"\n [config]=\"asCustom(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n </div>\n\n <!-- Custom Footer Actions -->\n <div *ngIf=\"hasCustomFooterActions\" class=\"flex gap-3 p-6 border-t border-gray-200\">\n <ng-container *ngFor=\"let action of leftFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n\n <div class=\"flex-1\"></div>\n\n <ng-container *ngFor=\"let action of rightFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }] });
|
|
4252
4789
|
}
|
|
4253
4790
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
4254
4791
|
type: Component,
|
|
@@ -4259,7 +4796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4259
4796
|
MnConfirmationBodyComponent,
|
|
4260
4797
|
MnCustomBodyHostComponent,
|
|
4261
4798
|
MnButton,
|
|
4262
|
-
], template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col
|
|
4799
|
+
], template: "<div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" *ngIf=\"showBackdrop\" (click)=\"onBackdropClick()\"></div>\n\n<div\n class=\"modal-container relative bg-white rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [ngClass]=\"[containerSizeClass, animationClass]\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n <div class=\"flex items-center justify-between p-6 border-b border-gray-200\">\n <div class=\"flex flex-col gap-0.5\">\n <h2 class=\"m-0 text-xl font-semibold text-gray-900\" *ngIf=\"config.title\" id=\"mn-modal-title\">{{ config.title }}</h2>\n <p class=\"m-0 text-sm text-gray-500 font-normal\" *ngIf=\"config.subtitle\">{{ config.subtitle }}</p>\n </div>\n <button\n *ngIf=\"showCloseButton\"\n class=\"bg-transparent border-none text-2xl cursor-pointer text-gray-500 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-gray-100 hover:text-gray-900\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n </div>\n <p class=\"m-0 px-6 text-sm text-gray-500 leading-relaxed\" *ngIf=\"config.description\" id=\"mn-modal-description\">{{ config.description }}</p>\n\n <div class=\"flex-1 overflow-y-auto p-6\">\n <mn-wizard-body\n *ngIf=\"config.kind === ModalKind.WIZARD\"\n [config]=\"asWizard(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-wizard-body>\n\n <mn-form-body\n *ngIf=\"config.kind === ModalKind.FORM\"\n [config]=\"asForm(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-form-body>\n\n <mn-confirmation-body\n *ngIf=\"config.kind === ModalKind.CONFIRMATION\"\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-confirmation-body>\n\n <mn-custom-body-host\n *ngIf=\"config.kind === ModalKind.CUSTOM\"\n [config]=\"asCustom(config)\"\n [modalRef]=\"asAny(modalRef)\"\n ></mn-custom-body-host>\n </div>\n\n <!-- Custom Footer Actions -->\n <div *ngIf=\"hasCustomFooterActions\" class=\"flex gap-3 p-6 border-t border-gray-200\">\n <ng-container *ngFor=\"let action of leftFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n\n <div class=\"flex-1\"></div>\n\n <ng-container *ngFor=\"let action of rightFooterActions\">\n <button\n mnButton\n [data]=\"{\n variant: getActionButtonVariant(action.style),\n color: getActionButtonColor(action.style),\n disabled: action.disabled\n }\"\n [disabled]=\"action.disabled\"\n (click)=\"onFooterAction(action)\"\n >\n {{ action.label }}\n </button>\n </ng-container>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}\n"] }]
|
|
4263
4800
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { config: [{
|
|
4264
4801
|
type: Input
|
|
4265
4802
|
}], modalRef: [{
|
|
@@ -4275,6 +4812,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4275
4812
|
class MnModalService {
|
|
4276
4813
|
appRef = inject(ApplicationRef);
|
|
4277
4814
|
injector = inject(EnvironmentInjector);
|
|
4815
|
+
modalStack = [];
|
|
4278
4816
|
open(config) {
|
|
4279
4817
|
// Create the modal shell component
|
|
4280
4818
|
const componentRef = createComponent(MnModalShellComponent, {
|
|
@@ -4285,6 +4823,12 @@ class MnModalService {
|
|
|
4285
4823
|
// Create modal ref
|
|
4286
4824
|
const modalRef = new MnModalRef(componentRef, config);
|
|
4287
4825
|
componentRef.instance.modalRef = modalRef;
|
|
4826
|
+
// Update stack and dim previous modal
|
|
4827
|
+
if (this.modalStack.length > 0) {
|
|
4828
|
+
const prevModal = this.modalStack[this.modalStack.length - 1];
|
|
4829
|
+
prevModal.component.isStacked = true;
|
|
4830
|
+
}
|
|
4831
|
+
this.modalStack.push(modalRef);
|
|
4288
4832
|
// Attach to application
|
|
4289
4833
|
this.appRef.attachView(componentRef.hostView);
|
|
4290
4834
|
const domElem = componentRef.location.nativeElement;
|
|
@@ -4293,6 +4837,15 @@ class MnModalService {
|
|
|
4293
4837
|
modalRef.afterClosed$.subscribe(() => {
|
|
4294
4838
|
this.appRef.detachView(componentRef.hostView);
|
|
4295
4839
|
domElem.remove();
|
|
4840
|
+
// Update stack
|
|
4841
|
+
const index = this.modalStack.indexOf(modalRef);
|
|
4842
|
+
if (index > -1) {
|
|
4843
|
+
this.modalStack.splice(index, 1);
|
|
4844
|
+
if (this.modalStack.length > 0) {
|
|
4845
|
+
const topModal = this.modalStack[this.modalStack.length - 1];
|
|
4846
|
+
topModal.component.isStacked = false;
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4296
4849
|
});
|
|
4297
4850
|
return modalRef;
|
|
4298
4851
|
}
|
|
@@ -4308,6 +4861,1274 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4308
4861
|
|
|
4309
4862
|
// Types
|
|
4310
4863
|
|
|
4864
|
+
/**
|
|
4865
|
+
* Available calendar view modes.
|
|
4866
|
+
*/
|
|
4867
|
+
var CalendarView;
|
|
4868
|
+
(function (CalendarView) {
|
|
4869
|
+
CalendarView["MONTH"] = "MONTH";
|
|
4870
|
+
CalendarView["WEEK"] = "WEEK";
|
|
4871
|
+
CalendarView["DAY"] = "DAY";
|
|
4872
|
+
})(CalendarView || (CalendarView = {}));
|
|
4873
|
+
/**
|
|
4874
|
+
* Builds locale-derived day name arrays from a BCP 47 locale string.
|
|
4875
|
+
* Uses January 1 2024 (a Monday) as the reference date.
|
|
4876
|
+
*/
|
|
4877
|
+
function buildDayNames(locale) {
|
|
4878
|
+
const base = new Date(2024, 0, 1); // 2024-01-01 is a Monday
|
|
4879
|
+
const short = [];
|
|
4880
|
+
const long = [];
|
|
4881
|
+
for (let i = 0; i < 7; i++) {
|
|
4882
|
+
const d = new Date(base);
|
|
4883
|
+
d.setDate(base.getDate() + i);
|
|
4884
|
+
short.push(d.toLocaleDateString(locale, { weekday: 'short' }));
|
|
4885
|
+
long.push(d.toLocaleDateString(locale, { weekday: 'long' }));
|
|
4886
|
+
}
|
|
4887
|
+
return { short, long };
|
|
4888
|
+
}
|
|
4889
|
+
/** Default calendar configuration values. */
|
|
4890
|
+
const DEFAULT_CALENDAR_CONFIG = (() => {
|
|
4891
|
+
const locale = 'en-US';
|
|
4892
|
+
const names = buildDayNames(locale);
|
|
4893
|
+
return {
|
|
4894
|
+
startHour: 7,
|
|
4895
|
+
endHour: 22,
|
|
4896
|
+
locale,
|
|
4897
|
+
todayLabel: 'Today',
|
|
4898
|
+
upcomingEventsTitle: 'Upcoming events',
|
|
4899
|
+
viewLabels: { MONTH: 'Month', WEEK: 'Week', DAY: 'Day' },
|
|
4900
|
+
shortDayNames: names.short,
|
|
4901
|
+
longDayNames: names.long,
|
|
4902
|
+
mobileBreakpoint: 768,
|
|
4903
|
+
};
|
|
4904
|
+
})();
|
|
4905
|
+
/**
|
|
4906
|
+
* Injection token for the resolved calendar configuration.
|
|
4907
|
+
*
|
|
4908
|
+
* Prefer using {@link MN_CALENDAR_CONFIG} with `provideMnComponentConfig`
|
|
4909
|
+
* so that settings can be managed via `mn-config.json5`. This token is
|
|
4910
|
+
* kept for backward compatibility and manual `providers` usage.
|
|
4911
|
+
*
|
|
4912
|
+
* @example
|
|
4913
|
+
* ```ts
|
|
4914
|
+
* providers: [
|
|
4915
|
+
* { provide: CALENDAR_CONFIG, useValue: { startHour: 8, endHour: 20, locale: 'nl-NL' } }
|
|
4916
|
+
* ]
|
|
4917
|
+
* ```
|
|
4918
|
+
*/
|
|
4919
|
+
const CALENDAR_CONFIG = new InjectionToken('CalendarConfig', {
|
|
4920
|
+
providedIn: 'root',
|
|
4921
|
+
factory: () => DEFAULT_CALENDAR_CONFIG
|
|
4922
|
+
});
|
|
4923
|
+
/**
|
|
4924
|
+
* Injection token resolved via `MnConfigService` (the `mn-config.json5` system).
|
|
4925
|
+
*
|
|
4926
|
+
* Use the helper {@link provideMnCalendarConfig} in the component's `providers`
|
|
4927
|
+
* array so that calendar settings are read from the config file and support
|
|
4928
|
+
* `$translate` markers, section scoping, and instance-id overrides.
|
|
4929
|
+
*
|
|
4930
|
+
* Component name in the config file: `'mn-calendar'`.
|
|
4931
|
+
*
|
|
4932
|
+
* @example
|
|
4933
|
+
* ```json5
|
|
4934
|
+
* // mn-config.json5
|
|
4935
|
+
* {
|
|
4936
|
+
* defaults: {
|
|
4937
|
+
* "mn-calendar": {
|
|
4938
|
+
* startHour: 8,
|
|
4939
|
+
* endHour: 20,
|
|
4940
|
+
* locale: "nl-NL",
|
|
4941
|
+
* todayLabel: { $translate: "calendar.today" }
|
|
4942
|
+
* }
|
|
4943
|
+
* }
|
|
4944
|
+
* }
|
|
4945
|
+
* ```
|
|
4946
|
+
*/
|
|
4947
|
+
const MN_CALENDAR_CONFIG = new InjectionToken('MN_CALENDAR_CONFIG');
|
|
4948
|
+
/** Component name used to look up calendar settings in `mn-config.json5`. */
|
|
4949
|
+
const MN_CALENDAR_COMPONENT_NAME = 'mn-calendar';
|
|
4950
|
+
/**
|
|
4951
|
+
* Provider helper that wires the calendar into the `mn-config` system.
|
|
4952
|
+
*
|
|
4953
|
+
* Add this to the `providers` array of the component (or module) that hosts
|
|
4954
|
+
* `<app-calendar-view>`. It reads defaults and overrides from `mn-config.json5`
|
|
4955
|
+
* under the key `"mn-calendar"` and provides them via {@link MN_CALENDAR_CONFIG}.
|
|
4956
|
+
*
|
|
4957
|
+
* @param initial — optional partial defaults merged before config-file values.
|
|
4958
|
+
*/
|
|
4959
|
+
function provideMnCalendarConfig(initial) {
|
|
4960
|
+
return provideMnComponentConfig(MN_CALENDAR_CONFIG, MN_CALENDAR_COMPONENT_NAME, initial);
|
|
4961
|
+
}
|
|
4962
|
+
/**
|
|
4963
|
+
* Merges a partial config with defaults, re-deriving day names from locale when needed.
|
|
4964
|
+
*/
|
|
4965
|
+
function resolveCalendarConfig(partial) {
|
|
4966
|
+
if (!partial)
|
|
4967
|
+
return { ...DEFAULT_CALENDAR_CONFIG };
|
|
4968
|
+
const locale = partial.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
|
|
4969
|
+
const names = buildDayNames(locale);
|
|
4970
|
+
return {
|
|
4971
|
+
...DEFAULT_CALENDAR_CONFIG,
|
|
4972
|
+
...partial,
|
|
4973
|
+
locale,
|
|
4974
|
+
shortDayNames: partial.shortDayNames ?? names.short,
|
|
4975
|
+
longDayNames: partial.longDayNames ?? names.long,
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
/**
|
|
4980
|
+
* Injection token for the calendar date formatter.
|
|
4981
|
+
*
|
|
4982
|
+
* @example
|
|
4983
|
+
* ```ts
|
|
4984
|
+
* providers: [
|
|
4985
|
+
* { provide: CALENDAR_DATE_FORMATTER, useClass: MyCustomFormatter }
|
|
4986
|
+
* ]
|
|
4987
|
+
* ```
|
|
4988
|
+
*/
|
|
4989
|
+
const CALENDAR_DATE_FORMATTER = new InjectionToken('CalendarDateFormatter');
|
|
4990
|
+
|
|
4991
|
+
/**
|
|
4992
|
+
* Default implementation of {@link CalendarDateFormatter} that uses the
|
|
4993
|
+
* browser's `Intl.DateTimeFormat` API for locale-aware formatting.
|
|
4994
|
+
*
|
|
4995
|
+
* The locale is read from the injected {@link CALENDAR_CONFIG}. If no config
|
|
4996
|
+
* is provided, `'en-US'` is used as the fallback.
|
|
4997
|
+
*
|
|
4998
|
+
* This service has no dependency on `@ngx-translate` or any other i18n library,
|
|
4999
|
+
* so the calendar library works out of the box. Consumers can replace it with
|
|
5000
|
+
* their own implementation via the `CALENDAR_DATE_FORMATTER` injection token.
|
|
5001
|
+
*/
|
|
5002
|
+
class DefaultCalendarDateFormatter {
|
|
5003
|
+
locale;
|
|
5004
|
+
constructor(config) {
|
|
5005
|
+
this.locale = config?.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
|
|
5006
|
+
}
|
|
5007
|
+
/** Formats an hour and minute pair into a locale time string (e.g. "09:00 AM"). */
|
|
5008
|
+
formatTimeI(hour, minute) {
|
|
5009
|
+
const date = new Date();
|
|
5010
|
+
date.setHours(hour, minute, 0, 0);
|
|
5011
|
+
return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
|
|
5012
|
+
}
|
|
5013
|
+
/** Formats the time portion of a Date (e.g. "2:30 PM"). Returns empty string for undefined. */
|
|
5014
|
+
formatTime(date) {
|
|
5015
|
+
if (!date)
|
|
5016
|
+
return Promise.resolve('');
|
|
5017
|
+
return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
|
|
5018
|
+
}
|
|
5019
|
+
/** Formats a Date as a full date-time string (e.g. "May 15, 2026, 02:30 PM"). */
|
|
5020
|
+
formatDateTime(date) {
|
|
5021
|
+
return of(date.toLocaleString(this.locale, {
|
|
5022
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
5023
|
+
hour: '2-digit', minute: '2-digit'
|
|
5024
|
+
}));
|
|
5025
|
+
}
|
|
5026
|
+
/** Formats a Date as a date-only string (e.g. "May 15, 2026"). */
|
|
5027
|
+
formatDate(date) {
|
|
5028
|
+
return of(date.toLocaleDateString(this.locale, {
|
|
5029
|
+
year: 'numeric', month: 'short', day: 'numeric'
|
|
5030
|
+
}));
|
|
5031
|
+
}
|
|
5032
|
+
/** Formats a Date as `YYYY-MM-DD` for use in `<input type="date">` controls. */
|
|
5033
|
+
formatDateForFormControl(date) {
|
|
5034
|
+
const y = date.getFullYear();
|
|
5035
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
5036
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
5037
|
+
return `${y}-${m}-${d}`;
|
|
5038
|
+
}
|
|
5039
|
+
/** Returns `true` if both dates fall on the same calendar day. */
|
|
5040
|
+
isSameDay(date1, date2) {
|
|
5041
|
+
return date1.getFullYear() === date2.getFullYear()
|
|
5042
|
+
&& date1.getMonth() === date2.getMonth()
|
|
5043
|
+
&& date1.getDate() === date2.getDate();
|
|
5044
|
+
}
|
|
5045
|
+
/** Formats a Date as "Month Year" (e.g. "January 2026"). */
|
|
5046
|
+
formatMonthName(date) {
|
|
5047
|
+
return Promise.resolve(date.toLocaleString(this.locale, { month: 'long', year: 'numeric' }));
|
|
5048
|
+
}
|
|
5049
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, deps: [{ token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5050
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter });
|
|
5051
|
+
}
|
|
5052
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, decorators: [{
|
|
5053
|
+
type: Injectable
|
|
5054
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5055
|
+
type: Optional
|
|
5056
|
+
}, {
|
|
5057
|
+
type: Inject,
|
|
5058
|
+
args: [CALENDAR_CONFIG]
|
|
5059
|
+
}] }] });
|
|
5060
|
+
|
|
5061
|
+
/**
|
|
5062
|
+
* Month grid view showing a 7×6 grid of day cells.
|
|
5063
|
+
*
|
|
5064
|
+
* Each cell displays the day number and up to 3 coloured dots representing
|
|
5065
|
+
* events on that day. Clicking a cell emits `dayClicked`.
|
|
5066
|
+
*/
|
|
5067
|
+
class CalendarMonthComponent {
|
|
5068
|
+
/** The date whose month is displayed. */
|
|
5069
|
+
focusDay;
|
|
5070
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5071
|
+
eventsChanged;
|
|
5072
|
+
/** Observable that emits when the focus day changes. */
|
|
5073
|
+
focusDayChanged;
|
|
5074
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5075
|
+
config;
|
|
5076
|
+
/** Emits the date of a clicked day cell. */
|
|
5077
|
+
dayClicked = new EventEmitter();
|
|
5078
|
+
monthItems = [];
|
|
5079
|
+
longDayNames;
|
|
5080
|
+
events = [];
|
|
5081
|
+
destroy$ = new Subject();
|
|
5082
|
+
formatter;
|
|
5083
|
+
constructor() {
|
|
5084
|
+
this.formatter = new DefaultCalendarDateFormatter();
|
|
5085
|
+
this.longDayNames = DEFAULT_CALENDAR_CONFIG.longDayNames;
|
|
5086
|
+
}
|
|
5087
|
+
ngOnInit() {
|
|
5088
|
+
const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5089
|
+
this.longDayNames = resolved.longDayNames;
|
|
5090
|
+
this.buildMonth();
|
|
5091
|
+
if (this.eventsChanged) {
|
|
5092
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5093
|
+
this.events = events;
|
|
5094
|
+
this.buildMonth();
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
if (this.focusDayChanged) {
|
|
5098
|
+
this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
|
|
5099
|
+
this.focusDay = date;
|
|
5100
|
+
this.buildMonth();
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
ngOnDestroy() {
|
|
5105
|
+
this.destroy$.next();
|
|
5106
|
+
this.destroy$.complete();
|
|
5107
|
+
}
|
|
5108
|
+
/** Emits the clicked day's date. */
|
|
5109
|
+
onDayClick(date) {
|
|
5110
|
+
this.dayClicked.emit(date);
|
|
5111
|
+
}
|
|
5112
|
+
/** trackBy for day name headers. */
|
|
5113
|
+
trackByDayName(index) {
|
|
5114
|
+
return index;
|
|
5115
|
+
}
|
|
5116
|
+
/** trackBy for month grid cells. */
|
|
5117
|
+
trackByMonthItem(_index, item) {
|
|
5118
|
+
return item.date.getTime();
|
|
5119
|
+
}
|
|
5120
|
+
/** trackBy for event dots. */
|
|
5121
|
+
trackByEventDot(_index, event) {
|
|
5122
|
+
return event.id;
|
|
5123
|
+
}
|
|
5124
|
+
/** Builds the 42-cell month grid (6 rows × 7 columns). */
|
|
5125
|
+
buildMonth() {
|
|
5126
|
+
if (!this.focusDay)
|
|
5127
|
+
return;
|
|
5128
|
+
const year = this.focusDay.getFullYear();
|
|
5129
|
+
const month = this.focusDay.getMonth();
|
|
5130
|
+
const firstDay = new Date(year, month, 1);
|
|
5131
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
5132
|
+
let startOffset = firstDay.getDay() - 1;
|
|
5133
|
+
if (startOffset < 0)
|
|
5134
|
+
startOffset = 6;
|
|
5135
|
+
const today = new Date();
|
|
5136
|
+
this.monthItems = [];
|
|
5137
|
+
for (let i = startOffset - 1; i >= 0; i--) {
|
|
5138
|
+
const date = new Date(year, month, -i);
|
|
5139
|
+
this.monthItems.push(this.createMonthItem(date, false, today));
|
|
5140
|
+
}
|
|
5141
|
+
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
5142
|
+
const date = new Date(year, month, d);
|
|
5143
|
+
this.monthItems.push(this.createMonthItem(date, true, today));
|
|
5144
|
+
}
|
|
5145
|
+
const remaining = 42 - this.monthItems.length;
|
|
5146
|
+
for (let i = 1; i <= remaining; i++) {
|
|
5147
|
+
const date = new Date(year, month + 1, i);
|
|
5148
|
+
this.monthItems.push(this.createMonthItem(date, false, today));
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
createMonthItem(date, isCurrentMonth, today) {
|
|
5152
|
+
const isToday = this.formatter.isSameDay(date, today);
|
|
5153
|
+
const dayEvents = this.events.filter(e => this.formatter.isSameDay(e.startTime, date) ||
|
|
5154
|
+
this.formatter.isSameDay(e.endTime, date) ||
|
|
5155
|
+
(e.startTime < date && e.endTime > date));
|
|
5156
|
+
return {
|
|
5157
|
+
date,
|
|
5158
|
+
dayNumber: date.getDate(),
|
|
5159
|
+
isCurrentMonth,
|
|
5160
|
+
isToday,
|
|
5161
|
+
events: dayEvents
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5165
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "app-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\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"] }] });
|
|
5166
|
+
}
|
|
5167
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, decorators: [{
|
|
5168
|
+
type: Component,
|
|
5169
|
+
args: [{ selector: 'app-calendar-month', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\n"] }]
|
|
5170
|
+
}], ctorParameters: () => [], propDecorators: { focusDay: [{
|
|
5171
|
+
type: Input
|
|
5172
|
+
}], eventsChanged: [{
|
|
5173
|
+
type: Input
|
|
5174
|
+
}], focusDayChanged: [{
|
|
5175
|
+
type: Input
|
|
5176
|
+
}], config: [{
|
|
5177
|
+
type: Input
|
|
5178
|
+
}], dayClicked: [{
|
|
5179
|
+
type: Output
|
|
5180
|
+
}] } });
|
|
5181
|
+
|
|
5182
|
+
/**
|
|
5183
|
+
* Service that computes the visual layout of calendar events within a
|
|
5184
|
+
* time-grid (week or day view).
|
|
5185
|
+
*
|
|
5186
|
+
* Responsibilities:
|
|
5187
|
+
* - Splitting multi-day events into per-day segments.
|
|
5188
|
+
* - Assigning non-overlapping column indices to concurrent events.
|
|
5189
|
+
* - Computing the width (column span) each event should occupy.
|
|
5190
|
+
*
|
|
5191
|
+
* This service is stateless — all state is passed via method parameters.
|
|
5192
|
+
* Provide it per-component (not root) so each view gets its own instance.
|
|
5193
|
+
*/
|
|
5194
|
+
class CalendarEventLayoutService {
|
|
5195
|
+
/**
|
|
5196
|
+
* Returns `true` when two time ranges overlap (exclusive boundaries).
|
|
5197
|
+
*/
|
|
5198
|
+
eventsOverlap(startA, endA, startB, endB) {
|
|
5199
|
+
return startA < endB && startB < endA;
|
|
5200
|
+
}
|
|
5201
|
+
/**
|
|
5202
|
+
* Returns all events whose time range overlaps the given `[start, end)` window.
|
|
5203
|
+
*/
|
|
5204
|
+
getAllEventsOnSpecificTime(events, start, end) {
|
|
5205
|
+
return events.filter(e => this.eventsOverlap(e.startTime, e.endTime, start, end));
|
|
5206
|
+
}
|
|
5207
|
+
/**
|
|
5208
|
+
* Splits multi-day events into per-day segments that fit within the
|
|
5209
|
+
* visible hour range (`startHour`–`endHour`) and date range.
|
|
5210
|
+
*
|
|
5211
|
+
* Single-day events are shallow-copied as-is. Multi-day events produce
|
|
5212
|
+
* one segment per day with `continued` / `continuedEnd` flags set.
|
|
5213
|
+
*/
|
|
5214
|
+
calculateMultiDayEvents(events, startHour, endHour, rangeStart, rangeEnd) {
|
|
5215
|
+
const result = [];
|
|
5216
|
+
for (const event of events) {
|
|
5217
|
+
const eventStart = new Date(event.startTime);
|
|
5218
|
+
const eventEnd = new Date(event.endTime);
|
|
5219
|
+
if (eventStart.toDateString() === eventEnd.toDateString()) {
|
|
5220
|
+
result.push({ ...event });
|
|
5221
|
+
continue;
|
|
5222
|
+
}
|
|
5223
|
+
const current = new Date(eventStart);
|
|
5224
|
+
let isFirst = true;
|
|
5225
|
+
while (current < eventEnd && current < rangeEnd) {
|
|
5226
|
+
if (current >= rangeStart) {
|
|
5227
|
+
const dayStart = new Date(current);
|
|
5228
|
+
const dayEnd = new Date(current);
|
|
5229
|
+
if (isFirst) {
|
|
5230
|
+
dayEnd.setHours(endHour, 0, 0, 0);
|
|
5231
|
+
}
|
|
5232
|
+
else {
|
|
5233
|
+
dayStart.setHours(startHour, 0, 0, 0);
|
|
5234
|
+
if (current.toDateString() === eventEnd.toDateString()) {
|
|
5235
|
+
dayEnd.setHours(eventEnd.getHours(), eventEnd.getMinutes(), 0, 0);
|
|
5236
|
+
}
|
|
5237
|
+
else {
|
|
5238
|
+
dayEnd.setHours(endHour, 0, 0, 0);
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
result.push({
|
|
5242
|
+
...event,
|
|
5243
|
+
startTime: isFirst ? eventStart : dayStart,
|
|
5244
|
+
endTime: current.toDateString() === eventEnd.toDateString() ? eventEnd : dayEnd,
|
|
5245
|
+
continued: !isFirst,
|
|
5246
|
+
continuedEnd: current.toDateString() !== eventEnd.toDateString()
|
|
5247
|
+
});
|
|
5248
|
+
}
|
|
5249
|
+
isFirst = false;
|
|
5250
|
+
current.setDate(current.getDate() + 1);
|
|
5251
|
+
current.setHours(0, 0, 0, 0);
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
return result;
|
|
5255
|
+
}
|
|
5256
|
+
/**
|
|
5257
|
+
* Assigns a zero-based `column` index to each event so that overlapping
|
|
5258
|
+
* events occupy different columns.
|
|
5259
|
+
*
|
|
5260
|
+
* Events are processed in start-time order (longest duration first for ties).
|
|
5261
|
+
* Each event gets the earliest column not already occupied by an overlapping event.
|
|
5262
|
+
*/
|
|
5263
|
+
assignColumnsToEvents(events) {
|
|
5264
|
+
const sorted = [...events].sort((a, b) => {
|
|
5265
|
+
const diff = a.startTime.getTime() - b.startTime.getTime();
|
|
5266
|
+
if (diff !== 0)
|
|
5267
|
+
return diff;
|
|
5268
|
+
return (b.endTime.getTime() - b.startTime.getTime()) - (a.endTime.getTime() - a.startTime.getTime());
|
|
5269
|
+
});
|
|
5270
|
+
for (const event of sorted) {
|
|
5271
|
+
event.column = this.findEarliestPossibleColumn(event, sorted);
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
/**
|
|
5275
|
+
* Assigns a `width` (column span) to each event, expanding it to fill
|
|
5276
|
+
* unused columns to its right within the overlapping group.
|
|
5277
|
+
*/
|
|
5278
|
+
assignWidthsToEvents(events, scanStart, scanEnd) {
|
|
5279
|
+
for (const event of events) {
|
|
5280
|
+
const overlapping = this.getAllEventsOnSpecificTime(events, event.startTime, event.endTime);
|
|
5281
|
+
const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
|
|
5282
|
+
const biggestPossible = this.findBiggestPossibleWidth(event, events, scanStart, scanEnd);
|
|
5283
|
+
event.width = Math.max(1, biggestPossible);
|
|
5284
|
+
if ((event.column ?? 0) + event.width > maxCol + 1) {
|
|
5285
|
+
event.width = maxCol + 1 - (event.column ?? 0);
|
|
5286
|
+
}
|
|
5287
|
+
if (event.width < 1)
|
|
5288
|
+
event.width = 1;
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
/** Finds the lowest column index not occupied by any overlapping event. */
|
|
5292
|
+
findEarliestPossibleColumn(event, allEvents) {
|
|
5293
|
+
const overlapping = allEvents.filter(e => e !== event
|
|
5294
|
+
&& e.column !== undefined
|
|
5295
|
+
&& this.eventsOverlap(e.startTime, e.endTime, event.startTime, event.endTime));
|
|
5296
|
+
let column = 0;
|
|
5297
|
+
while (overlapping.some(e => e.column === column)) {
|
|
5298
|
+
column++;
|
|
5299
|
+
}
|
|
5300
|
+
return column;
|
|
5301
|
+
}
|
|
5302
|
+
/** Computes the maximum width an event can span without overlapping a neighbour to its right. */
|
|
5303
|
+
findBiggestPossibleWidth(event, allEvents, _scanStart, _scanEnd) {
|
|
5304
|
+
const overlapping = this.getAllEventsOnSpecificTime(allEvents, event.startTime, event.endTime);
|
|
5305
|
+
const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
|
|
5306
|
+
const totalColumns = maxCol + 1;
|
|
5307
|
+
const occupiedCols = overlapping
|
|
5308
|
+
.filter(e => e !== event && (e.column ?? 0) > (event.column ?? 0))
|
|
5309
|
+
.map(e => e.column ?? 0);
|
|
5310
|
+
if (occupiedCols.length === 0) {
|
|
5311
|
+
return totalColumns - (event.column ?? 0);
|
|
5312
|
+
}
|
|
5313
|
+
return Math.min(...occupiedCols) - (event.column ?? 0);
|
|
5314
|
+
}
|
|
5315
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
5316
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService });
|
|
5317
|
+
}
|
|
5318
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, decorators: [{
|
|
5319
|
+
type: Injectable
|
|
5320
|
+
}] });
|
|
5321
|
+
|
|
5322
|
+
/**
|
|
5323
|
+
* Static utility methods for calendar grid positioning.
|
|
5324
|
+
*/
|
|
5325
|
+
class CalendarUtility {
|
|
5326
|
+
/**
|
|
5327
|
+
* Converts a weekday (from `Date.getDay()`) to a 1-based Monday-first column index.
|
|
5328
|
+
* Monday = 1, Tuesday = 2, …, Sunday = 7.
|
|
5329
|
+
*/
|
|
5330
|
+
static getCorrectColumn(date) {
|
|
5331
|
+
const day = date.getDay();
|
|
5332
|
+
return day === 0 ? 7 : day;
|
|
5333
|
+
}
|
|
5334
|
+
/**
|
|
5335
|
+
* Converts an hour + minute pair to a 1-based CSS grid row index
|
|
5336
|
+
* within a half-hour grid starting at `startHour`.
|
|
5337
|
+
*
|
|
5338
|
+
* Each hour occupies two rows (one per 30-minute slot).
|
|
5339
|
+
* Formula: `(hour - startHour) * 2 + (minute >= 30 ? 1 : 0) + 1`
|
|
5340
|
+
*
|
|
5341
|
+
* @returns Grid row number (minimum 1).
|
|
5342
|
+
*/
|
|
5343
|
+
static getCorrectRow(hour, minute, startHour) {
|
|
5344
|
+
const hourOffset = hour - startHour;
|
|
5345
|
+
const row = hourOffset * 2 + (minute >= 30 ? 1 : 0) + 1;
|
|
5346
|
+
return Math.max(1, row);
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
/**
|
|
5351
|
+
* Default event renderer used when no custom component is provided.
|
|
5352
|
+
*
|
|
5353
|
+
* Displays the event title, formatted time range, and optional description
|
|
5354
|
+
* with the event's colour scheme applied as background and left-border accent.
|
|
5355
|
+
*/
|
|
5356
|
+
class CalendarEventDefaultComponent {
|
|
5357
|
+
/** The event to render. Set by {@link CalendarEventComponent} after creation. */
|
|
5358
|
+
event;
|
|
5359
|
+
formattedTime = '';
|
|
5360
|
+
formatter;
|
|
5361
|
+
constructor(formatter) {
|
|
5362
|
+
this.formatter = formatter ?? new DefaultCalendarDateFormatter();
|
|
5363
|
+
}
|
|
5364
|
+
async ngOnInit() {
|
|
5365
|
+
if (this.event) {
|
|
5366
|
+
const start = await this.formatter.formatTime(this.event.startTime);
|
|
5367
|
+
const end = await this.formatter.formatTime(this.event.endTime);
|
|
5368
|
+
this.formattedTime = `${start} - ${end}`;
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
5372
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventDefaultComponent, isStandalone: true, selector: "app-calendar-event-default", ngImport: i0, template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
5373
|
+
}
|
|
5374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, decorators: [{
|
|
5375
|
+
type: Component,
|
|
5376
|
+
args: [{ selector: 'app-calendar-event-default', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
5377
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5378
|
+
type: Optional
|
|
5379
|
+
}, {
|
|
5380
|
+
type: Inject,
|
|
5381
|
+
args: [CALENDAR_DATE_FORMATTER]
|
|
5382
|
+
}] }] });
|
|
5383
|
+
|
|
5384
|
+
/**
|
|
5385
|
+
* Dynamic event renderer that injects a custom or default event component
|
|
5386
|
+
* into its view container.
|
|
5387
|
+
*
|
|
5388
|
+
* The component to render is resolved in this order:
|
|
5389
|
+
* 1. `customComponent` input (set on the parent week/day view)
|
|
5390
|
+
* 2. `event.component` (per-event override)
|
|
5391
|
+
* 3. {@link CalendarEventDefaultComponent} (library default)
|
|
5392
|
+
*/
|
|
5393
|
+
class CalendarEventComponent {
|
|
5394
|
+
/** The event data to render. */
|
|
5395
|
+
event;
|
|
5396
|
+
/** Optional custom component type that overrides the default renderer. */
|
|
5397
|
+
customComponent;
|
|
5398
|
+
/** Emits when the rendered event is clicked. */
|
|
5399
|
+
eventClicked = new EventEmitter();
|
|
5400
|
+
eventContainer;
|
|
5401
|
+
rendered = false;
|
|
5402
|
+
ngAfterViewInit() {
|
|
5403
|
+
this.renderComponent();
|
|
5404
|
+
}
|
|
5405
|
+
ngOnChanges(changes) {
|
|
5406
|
+
if (this.rendered && (changes['event'] || changes['customComponent'])) {
|
|
5407
|
+
this.renderComponent();
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
/** Emits the event click. */
|
|
5411
|
+
onEventClick() {
|
|
5412
|
+
this.eventClicked.emit(this.event);
|
|
5413
|
+
}
|
|
5414
|
+
/** Creates the event component dynamically and sets its `event` property. */
|
|
5415
|
+
renderComponent() {
|
|
5416
|
+
if (!this.eventContainer)
|
|
5417
|
+
return;
|
|
5418
|
+
this.eventContainer.clear();
|
|
5419
|
+
const component = this.customComponent ?? this.event?.component ?? CalendarEventDefaultComponent;
|
|
5420
|
+
const ref = this.eventContainer.createComponent(component);
|
|
5421
|
+
ref.instance.event = this.event;
|
|
5422
|
+
ref.changeDetectorRef.detectChanges();
|
|
5423
|
+
this.rendered = true;
|
|
5424
|
+
}
|
|
5425
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5426
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventComponent, isStandalone: true, selector: "app-calendar-event", inputs: { event: "event", customComponent: "customComponent" }, outputs: { eventClicked: "eventClicked" }, viewQueries: [{ propertyName: "eventContainer", first: true, predicate: ["eventContainer"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
5427
|
+
}
|
|
5428
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, decorators: [{
|
|
5429
|
+
type: Component,
|
|
5430
|
+
args: [{ selector: 'app-calendar-event', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"] }]
|
|
5431
|
+
}], propDecorators: { event: [{
|
|
5432
|
+
type: Input
|
|
5433
|
+
}], customComponent: [{
|
|
5434
|
+
type: Input
|
|
5435
|
+
}], eventClicked: [{
|
|
5436
|
+
type: Output
|
|
5437
|
+
}], eventContainer: [{
|
|
5438
|
+
type: ViewChild,
|
|
5439
|
+
args: ['eventContainer', { read: ViewContainerRef, static: true }]
|
|
5440
|
+
}] } });
|
|
5441
|
+
|
|
5442
|
+
/**
|
|
5443
|
+
* Week grid view showing 7 day columns with half-hour time slots.
|
|
5444
|
+
*
|
|
5445
|
+
* Overlapping events within the same day are laid out in sub-columns
|
|
5446
|
+
* so they appear side-by-side rather than stacked.
|
|
5447
|
+
*/
|
|
5448
|
+
class CalendarWeekComponent {
|
|
5449
|
+
layoutService;
|
|
5450
|
+
/** The date around which the week is centred. */
|
|
5451
|
+
focusDay;
|
|
5452
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5453
|
+
eventsChanged;
|
|
5454
|
+
/** Observable that emits when the focus day changes. */
|
|
5455
|
+
focusDayChanged;
|
|
5456
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5457
|
+
config;
|
|
5458
|
+
/** Optional custom event renderer component. */
|
|
5459
|
+
calendarEventComponent;
|
|
5460
|
+
/** Emits when a calendar event is clicked. */
|
|
5461
|
+
eventClicked = new EventEmitter();
|
|
5462
|
+
columns = [];
|
|
5463
|
+
hourRows = [];
|
|
5464
|
+
displayEvents = [];
|
|
5465
|
+
totalRows = 0;
|
|
5466
|
+
currentTimeRow = 0;
|
|
5467
|
+
currentTimeCol = '';
|
|
5468
|
+
gridTemplateColumns = 'repeat(7, 1fr)';
|
|
5469
|
+
dayColumnMap = [];
|
|
5470
|
+
events = [];
|
|
5471
|
+
destroy$ = new Subject();
|
|
5472
|
+
formatter;
|
|
5473
|
+
resolvedConfig;
|
|
5474
|
+
currentTimeInterval;
|
|
5475
|
+
constructor(layoutService) {
|
|
5476
|
+
this.layoutService = layoutService;
|
|
5477
|
+
this.formatter = new DefaultCalendarDateFormatter();
|
|
5478
|
+
}
|
|
5479
|
+
ngOnInit() {
|
|
5480
|
+
this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5481
|
+
this.buildHourRows();
|
|
5482
|
+
this.buildColumns();
|
|
5483
|
+
this.updateCurrentTime();
|
|
5484
|
+
this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
|
|
5485
|
+
if (this.eventsChanged) {
|
|
5486
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5487
|
+
this.events = events;
|
|
5488
|
+
this.refreshEvents();
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
if (this.focusDayChanged) {
|
|
5492
|
+
this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
|
|
5493
|
+
this.focusDay = date;
|
|
5494
|
+
this.buildColumns();
|
|
5495
|
+
this.refreshEvents();
|
|
5496
|
+
this.updateCurrentTime();
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
}
|
|
5500
|
+
ngOnDestroy() {
|
|
5501
|
+
this.destroy$.next();
|
|
5502
|
+
this.destroy$.complete();
|
|
5503
|
+
if (this.currentTimeInterval)
|
|
5504
|
+
clearInterval(this.currentTimeInterval);
|
|
5505
|
+
}
|
|
5506
|
+
/** Returns the CSS `grid-row` value for an event based on its start/end times. */
|
|
5507
|
+
getEventRow(event) {
|
|
5508
|
+
const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5509
|
+
const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5510
|
+
return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
|
|
5511
|
+
}
|
|
5512
|
+
/** Returns the CSS `grid-column` span for a day header, accounting for sub-columns. */
|
|
5513
|
+
getHeaderColumn(dayIndex) {
|
|
5514
|
+
if (!this.dayColumnMap.length)
|
|
5515
|
+
return `${dayIndex + 2} / span 1`;
|
|
5516
|
+
const dayInfo = this.dayColumnMap[dayIndex];
|
|
5517
|
+
return `${dayInfo.startCol + 1} / span ${dayInfo.subColumns}`;
|
|
5518
|
+
}
|
|
5519
|
+
/** Returns the CSS `grid-column` value for an event within its day's sub-columns. */
|
|
5520
|
+
getEventColumn(event) {
|
|
5521
|
+
const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, event.startTime));
|
|
5522
|
+
if (dayIdx < 0)
|
|
5523
|
+
return '1 / span 1';
|
|
5524
|
+
const dayInfo = this.dayColumnMap[dayIdx];
|
|
5525
|
+
const subCol = (event.column ?? 0) + dayInfo.startCol;
|
|
5526
|
+
const width = event.width ?? 1;
|
|
5527
|
+
return `${subCol} / span ${width}`;
|
|
5528
|
+
}
|
|
5529
|
+
/** Forwards event click to parent. */
|
|
5530
|
+
onEventClick(event) {
|
|
5531
|
+
this.eventClicked.emit(event);
|
|
5532
|
+
}
|
|
5533
|
+
/** trackBy for hour rows. */
|
|
5534
|
+
trackByHour(_index, row) {
|
|
5535
|
+
return row.hour;
|
|
5536
|
+
}
|
|
5537
|
+
/** trackBy for day columns. */
|
|
5538
|
+
trackByColumn(_index, col) {
|
|
5539
|
+
return col.date.getTime();
|
|
5540
|
+
}
|
|
5541
|
+
/** trackBy for events. */
|
|
5542
|
+
trackByEvent(_index, event) {
|
|
5543
|
+
return event.id;
|
|
5544
|
+
}
|
|
5545
|
+
async buildHourRows() {
|
|
5546
|
+
this.hourRows = [];
|
|
5547
|
+
const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
|
|
5548
|
+
this.totalRows = hours * 2;
|
|
5549
|
+
for (let i = 0; i < hours; i++) {
|
|
5550
|
+
const hour = this.resolvedConfig.startHour + i;
|
|
5551
|
+
const label = await this.formatter.formatTimeI(hour, 0);
|
|
5552
|
+
this.hourRows.push({
|
|
5553
|
+
hour,
|
|
5554
|
+
topRow: i * 2 + 1,
|
|
5555
|
+
bottomRow: i * 2 + 3,
|
|
5556
|
+
hourLabel: label
|
|
5557
|
+
});
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
/** Builds the 7 day columns for the current week (Monday–Sunday). */
|
|
5561
|
+
buildColumns() {
|
|
5562
|
+
if (!this.focusDay)
|
|
5563
|
+
return;
|
|
5564
|
+
const shortNames = this.resolvedConfig.shortDayNames;
|
|
5565
|
+
const today = new Date();
|
|
5566
|
+
const day = this.focusDay.getDay();
|
|
5567
|
+
const mondayOffset = day === 0 ? -6 : 1 - day;
|
|
5568
|
+
const monday = new Date(this.focusDay);
|
|
5569
|
+
monday.setDate(this.focusDay.getDate() + mondayOffset);
|
|
5570
|
+
this.columns = [];
|
|
5571
|
+
for (let i = 0; i < 7; i++) {
|
|
5572
|
+
const date = new Date(monday);
|
|
5573
|
+
date.setDate(monday.getDate() + i);
|
|
5574
|
+
this.columns.push({
|
|
5575
|
+
date,
|
|
5576
|
+
dayName: shortNames[i],
|
|
5577
|
+
dayNumber: date.getDate(),
|
|
5578
|
+
isToday: this.formatter.isSameDay(date, today)
|
|
5579
|
+
});
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
/** Filters, splits, and lays out events for the current week. */
|
|
5583
|
+
refreshEvents() {
|
|
5584
|
+
if (!this.columns.length)
|
|
5585
|
+
return;
|
|
5586
|
+
const rangeStart = this.columns[0].date;
|
|
5587
|
+
const rangeEnd = new Date(this.columns[6].date);
|
|
5588
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
5589
|
+
const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
|
|
5590
|
+
this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
|
|
5591
|
+
// Assign columns per day so overlapping events within a day get sub-columns
|
|
5592
|
+
for (let i = 0; i < 7; i++) {
|
|
5593
|
+
const dayStart = new Date(this.columns[i].date);
|
|
5594
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
5595
|
+
const dayEnd = new Date(this.columns[i].date);
|
|
5596
|
+
dayEnd.setHours(23, 59, 59, 999);
|
|
5597
|
+
const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
|
|
5598
|
+
this.layoutService.assignColumnsToEvents(dayEvents);
|
|
5599
|
+
this.layoutService.assignWidthsToEvents(dayEvents, dayStart, dayEnd);
|
|
5600
|
+
}
|
|
5601
|
+
this.buildGridColumns();
|
|
5602
|
+
this.updateCurrentTime();
|
|
5603
|
+
}
|
|
5604
|
+
/** Computes the CSS grid-template-columns string based on per-day sub-column counts. */
|
|
5605
|
+
buildGridColumns() {
|
|
5606
|
+
this.dayColumnMap = [];
|
|
5607
|
+
let currentCol = 1;
|
|
5608
|
+
for (let i = 0; i < 7; i++) {
|
|
5609
|
+
const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
|
|
5610
|
+
let maxSubCols = 1;
|
|
5611
|
+
for (const e of dayEvents) {
|
|
5612
|
+
maxSubCols = Math.max(maxSubCols, (e.column ?? 0) + (e.width ?? 1));
|
|
5613
|
+
}
|
|
5614
|
+
this.dayColumnMap.push({ subColumns: maxSubCols, startCol: currentCol });
|
|
5615
|
+
currentCol += maxSubCols;
|
|
5616
|
+
}
|
|
5617
|
+
const parts = [];
|
|
5618
|
+
for (const day of this.dayColumnMap) {
|
|
5619
|
+
for (let j = 0; j < day.subColumns; j++) {
|
|
5620
|
+
parts.push(`${1 / day.subColumns}fr`);
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
this.gridTemplateColumns = parts.join(' ');
|
|
5624
|
+
}
|
|
5625
|
+
/** Updates the current-time red line position. */
|
|
5626
|
+
updateCurrentTime() {
|
|
5627
|
+
const now = new Date();
|
|
5628
|
+
const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, now));
|
|
5629
|
+
if (dayIdx >= 0 && this.dayColumnMap.length > 0) {
|
|
5630
|
+
const dayInfo = this.dayColumnMap[dayIdx];
|
|
5631
|
+
this.currentTimeCol = `${dayInfo.startCol} / span ${dayInfo.subColumns}`;
|
|
5632
|
+
this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
|
|
5633
|
+
}
|
|
5634
|
+
else {
|
|
5635
|
+
this.currentTimeCol = '';
|
|
5636
|
+
this.currentTimeRow = 0;
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
|
|
5640
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "app-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\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: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
|
|
5641
|
+
}
|
|
5642
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, decorators: [{
|
|
5643
|
+
type: Component,
|
|
5644
|
+
args: [{ selector: 'app-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
|
|
5645
|
+
}], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
|
|
5646
|
+
type: Input
|
|
5647
|
+
}], eventsChanged: [{
|
|
5648
|
+
type: Input
|
|
5649
|
+
}], focusDayChanged: [{
|
|
5650
|
+
type: Input
|
|
5651
|
+
}], config: [{
|
|
5652
|
+
type: Input
|
|
5653
|
+
}], calendarEventComponent: [{
|
|
5654
|
+
type: Input
|
|
5655
|
+
}], eventClicked: [{
|
|
5656
|
+
type: Output
|
|
5657
|
+
}] } });
|
|
5658
|
+
|
|
5659
|
+
/**
|
|
5660
|
+
* Day grid view showing a single day with half-hour time slots.
|
|
5661
|
+
*
|
|
5662
|
+
* Shares the same layout algorithm as the week view via
|
|
5663
|
+
* {@link CalendarEventLayoutService}.
|
|
5664
|
+
*/
|
|
5665
|
+
class CalendarDayComponent {
|
|
5666
|
+
layoutService;
|
|
5667
|
+
/** The date to display. */
|
|
5668
|
+
focusDay;
|
|
5669
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5670
|
+
eventsChanged;
|
|
5671
|
+
/** Observable that emits when the focus day changes. */
|
|
5672
|
+
focusDayChanged;
|
|
5673
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5674
|
+
config;
|
|
5675
|
+
/** Optional custom event renderer component. */
|
|
5676
|
+
calendarEventComponent;
|
|
5677
|
+
/** Emits when a calendar event is clicked. */
|
|
5678
|
+
eventClicked = new EventEmitter();
|
|
5679
|
+
hourRows = [];
|
|
5680
|
+
displayEvents = [];
|
|
5681
|
+
totalRows = 0;
|
|
5682
|
+
totalColumns = 1;
|
|
5683
|
+
currentTimeRow = 0;
|
|
5684
|
+
isToday = false;
|
|
5685
|
+
dayName = '';
|
|
5686
|
+
events = [];
|
|
5687
|
+
destroy$ = new Subject();
|
|
5688
|
+
formatter;
|
|
5689
|
+
resolvedConfig;
|
|
5690
|
+
currentTimeInterval;
|
|
5691
|
+
constructor(layoutService) {
|
|
5692
|
+
this.layoutService = layoutService;
|
|
5693
|
+
this.formatter = new DefaultCalendarDateFormatter();
|
|
5694
|
+
}
|
|
5695
|
+
ngOnInit() {
|
|
5696
|
+
this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5697
|
+
this.buildHourRows();
|
|
5698
|
+
this.updateDayInfo();
|
|
5699
|
+
this.updateCurrentTime();
|
|
5700
|
+
this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
|
|
5701
|
+
if (this.eventsChanged) {
|
|
5702
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5703
|
+
this.events = events;
|
|
5704
|
+
this.refreshEvents();
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
if (this.focusDayChanged) {
|
|
5708
|
+
this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
|
|
5709
|
+
this.focusDay = date;
|
|
5710
|
+
this.updateDayInfo();
|
|
5711
|
+
this.refreshEvents();
|
|
5712
|
+
this.updateCurrentTime();
|
|
5713
|
+
});
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
ngOnDestroy() {
|
|
5717
|
+
this.destroy$.next();
|
|
5718
|
+
this.destroy$.complete();
|
|
5719
|
+
if (this.currentTimeInterval)
|
|
5720
|
+
clearInterval(this.currentTimeInterval);
|
|
5721
|
+
}
|
|
5722
|
+
/** Returns the CSS `grid-row` value for an event. */
|
|
5723
|
+
getEventRow(event) {
|
|
5724
|
+
const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5725
|
+
const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
|
|
5726
|
+
return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
|
|
5727
|
+
}
|
|
5728
|
+
/** Returns the CSS `grid-column` value for an event within its sub-columns. */
|
|
5729
|
+
getEventColumn(event) {
|
|
5730
|
+
const col = (event.column ?? 0) + 1;
|
|
5731
|
+
const width = event.width ?? 1;
|
|
5732
|
+
return `${col} / span ${width}`;
|
|
5733
|
+
}
|
|
5734
|
+
/** Forwards event click to parent. */
|
|
5735
|
+
onEventClick(event) {
|
|
5736
|
+
this.eventClicked.emit(event);
|
|
5737
|
+
}
|
|
5738
|
+
/** trackBy for hour rows. */
|
|
5739
|
+
trackByHour(_index, row) {
|
|
5740
|
+
return row.hour;
|
|
5741
|
+
}
|
|
5742
|
+
/** trackBy for events. */
|
|
5743
|
+
trackByEvent(_index, event) {
|
|
5744
|
+
return event.id;
|
|
5745
|
+
}
|
|
5746
|
+
async buildHourRows() {
|
|
5747
|
+
this.hourRows = [];
|
|
5748
|
+
const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
|
|
5749
|
+
this.totalRows = hours * 2;
|
|
5750
|
+
for (let i = 0; i < hours; i++) {
|
|
5751
|
+
const hour = this.resolvedConfig.startHour + i;
|
|
5752
|
+
const label = await this.formatter.formatTimeI(hour, 0);
|
|
5753
|
+
this.hourRows.push({
|
|
5754
|
+
hour,
|
|
5755
|
+
topRow: i * 2 + 1,
|
|
5756
|
+
bottomRow: i * 2 + 3,
|
|
5757
|
+
hourLabel: label
|
|
5758
|
+
});
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
/** Updates the day name and isToday flag. */
|
|
5762
|
+
updateDayInfo() {
|
|
5763
|
+
if (!this.focusDay)
|
|
5764
|
+
return;
|
|
5765
|
+
const today = new Date();
|
|
5766
|
+
this.isToday = this.formatter.isSameDay(this.focusDay, today);
|
|
5767
|
+
const longNames = this.resolvedConfig.longDayNames;
|
|
5768
|
+
const dayIdx = this.focusDay.getDay();
|
|
5769
|
+
const mondayIdx = dayIdx === 0 ? 6 : dayIdx - 1;
|
|
5770
|
+
this.dayName = longNames[mondayIdx];
|
|
5771
|
+
}
|
|
5772
|
+
/** Filters, splits, and lays out events for the focus day. */
|
|
5773
|
+
refreshEvents() {
|
|
5774
|
+
if (!this.focusDay)
|
|
5775
|
+
return;
|
|
5776
|
+
const rangeStart = new Date(this.focusDay);
|
|
5777
|
+
rangeStart.setHours(0, 0, 0, 0);
|
|
5778
|
+
const rangeEnd = new Date(this.focusDay);
|
|
5779
|
+
rangeEnd.setHours(23, 59, 59, 999);
|
|
5780
|
+
const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
|
|
5781
|
+
this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
|
|
5782
|
+
this.layoutService.assignColumnsToEvents(this.displayEvents);
|
|
5783
|
+
this.layoutService.assignWidthsToEvents(this.displayEvents, rangeStart, rangeEnd);
|
|
5784
|
+
const maxCol = this.displayEvents.reduce((max, e) => Math.max(max, (e.column ?? 0) + (e.width ?? 1)), 1);
|
|
5785
|
+
this.totalColumns = maxCol;
|
|
5786
|
+
}
|
|
5787
|
+
/** Updates the current-time red line position. */
|
|
5788
|
+
updateCurrentTime() {
|
|
5789
|
+
const now = new Date();
|
|
5790
|
+
if (this.focusDay && this.formatter.isSameDay(this.focusDay, now)) {
|
|
5791
|
+
this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
|
|
5792
|
+
this.isToday = true;
|
|
5793
|
+
}
|
|
5794
|
+
else {
|
|
5795
|
+
this.currentTimeRow = 0;
|
|
5796
|
+
this.isToday = false;
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
|
|
5800
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "app-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\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: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
|
|
5801
|
+
}
|
|
5802
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, decorators: [{
|
|
5803
|
+
type: Component,
|
|
5804
|
+
args: [{ selector: 'app-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
|
|
5805
|
+
}], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
|
|
5806
|
+
type: Input
|
|
5807
|
+
}], eventsChanged: [{
|
|
5808
|
+
type: Input
|
|
5809
|
+
}], focusDayChanged: [{
|
|
5810
|
+
type: Input
|
|
5811
|
+
}], config: [{
|
|
5812
|
+
type: Input
|
|
5813
|
+
}], calendarEventComponent: [{
|
|
5814
|
+
type: Input
|
|
5815
|
+
}], eventClicked: [{
|
|
5816
|
+
type: Output
|
|
5817
|
+
}] } });
|
|
5818
|
+
|
|
5819
|
+
/**
|
|
5820
|
+
* Renders a single row in the upcoming-events sidebar.
|
|
5821
|
+
* Shows the event title, formatted date/time, and optional description.
|
|
5822
|
+
*/
|
|
5823
|
+
class UpcomingEventRowComponent {
|
|
5824
|
+
/** The event to display. */
|
|
5825
|
+
event;
|
|
5826
|
+
/** Emits the event when this row is clicked. */
|
|
5827
|
+
eventClicked = new EventEmitter();
|
|
5828
|
+
formattedDate = '';
|
|
5829
|
+
formatter;
|
|
5830
|
+
constructor(formatter) {
|
|
5831
|
+
this.formatter = formatter ?? new DefaultCalendarDateFormatter();
|
|
5832
|
+
}
|
|
5833
|
+
async ngOnInit() {
|
|
5834
|
+
if (this.event) {
|
|
5835
|
+
const start = await this.formatter.formatTime(this.event.startTime);
|
|
5836
|
+
const end = await this.formatter.formatTime(this.event.endTime);
|
|
5837
|
+
this.formattedDate = `${start} - ${end}`;
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
5841
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventRowComponent, isStandalone: true, selector: "app-upcoming-event-row", inputs: { event: "event" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
5842
|
+
}
|
|
5843
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, decorators: [{
|
|
5844
|
+
type: Component,
|
|
5845
|
+
args: [{ selector: 'app-upcoming-event-row', standalone: true, imports: [CommonModule], template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
5846
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5847
|
+
type: Optional
|
|
5848
|
+
}, {
|
|
5849
|
+
type: Inject,
|
|
5850
|
+
args: [CALENDAR_DATE_FORMATTER]
|
|
5851
|
+
}] }], propDecorators: { event: [{
|
|
5852
|
+
type: Input
|
|
5853
|
+
}], eventClicked: [{
|
|
5854
|
+
type: Output
|
|
5855
|
+
}] } });
|
|
5856
|
+
|
|
5857
|
+
/**
|
|
5858
|
+
* Sidebar component that lists the next 10 upcoming events
|
|
5859
|
+
* (events whose end time is in the future), sorted by start time.
|
|
5860
|
+
*/
|
|
5861
|
+
class UpcomingEventsComponent {
|
|
5862
|
+
/** Observable that emits the full event list whenever it changes. */
|
|
5863
|
+
eventsChanged;
|
|
5864
|
+
/** Resolved calendar configuration passed from the parent view. */
|
|
5865
|
+
config;
|
|
5866
|
+
/** Emits when an upcoming event row is clicked. */
|
|
5867
|
+
eventClicked = new EventEmitter();
|
|
5868
|
+
upcomingEvents = [];
|
|
5869
|
+
title;
|
|
5870
|
+
destroy$ = new Subject();
|
|
5871
|
+
constructor() {
|
|
5872
|
+
this.title = DEFAULT_CALENDAR_CONFIG.upcomingEventsTitle;
|
|
5873
|
+
}
|
|
5874
|
+
ngOnInit() {
|
|
5875
|
+
const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
5876
|
+
this.title = resolved.upcomingEventsTitle;
|
|
5877
|
+
if (this.eventsChanged) {
|
|
5878
|
+
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5879
|
+
const now = new Date();
|
|
5880
|
+
this.upcomingEvents = events
|
|
5881
|
+
.filter(e => e.endTime > now)
|
|
5882
|
+
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
|
5883
|
+
.slice(0, 10);
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
ngOnDestroy() {
|
|
5888
|
+
this.destroy$.next();
|
|
5889
|
+
this.destroy$.complete();
|
|
5890
|
+
}
|
|
5891
|
+
/** trackBy for upcoming event rows. */
|
|
5892
|
+
trackByEvent(_index, event) {
|
|
5893
|
+
return event.id;
|
|
5894
|
+
}
|
|
5895
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5896
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "app-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\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: "component", type: UpcomingEventRowComponent, selector: "app-upcoming-event-row", inputs: ["event"], outputs: ["eventClicked"] }] });
|
|
5897
|
+
}
|
|
5898
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, decorators: [{
|
|
5899
|
+
type: Component,
|
|
5900
|
+
args: [{ selector: 'app-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\n"] }]
|
|
5901
|
+
}], ctorParameters: () => [], propDecorators: { eventsChanged: [{
|
|
5902
|
+
type: Input
|
|
5903
|
+
}], config: [{
|
|
5904
|
+
type: Input
|
|
5905
|
+
}], eventClicked: [{
|
|
5906
|
+
type: Output
|
|
5907
|
+
}] } });
|
|
5908
|
+
|
|
5909
|
+
/**
|
|
5910
|
+
* Main calendar orchestrator component.
|
|
5911
|
+
*
|
|
5912
|
+
* Provides a toolbar with view switching (month / week / day), date navigation,
|
|
5913
|
+
* and an optional action button. The active view and an upcoming-events sidebar
|
|
5914
|
+
* are rendered inside a responsive grid layout.
|
|
5915
|
+
*
|
|
5916
|
+
* All configuration (visible hours, locale, labels, mobile breakpoint) is read
|
|
5917
|
+
* from the `mn-config.json5` system via {@link MN_CALENDAR_CONFIG}, falling back
|
|
5918
|
+
* to the legacy {@link CALENDAR_CONFIG} injection token, then to built-in defaults.
|
|
5919
|
+
* Date formatting is delegated to the {@link CALENDAR_DATE_FORMATTER} token.
|
|
5920
|
+
*
|
|
5921
|
+
* @example
|
|
5922
|
+
* ```html
|
|
5923
|
+
* <app-calendar-view
|
|
5924
|
+
* [showButton]="true"
|
|
5925
|
+
* [buttonTitle]="'New Event'"
|
|
5926
|
+
* [NewCalendarItemsEvent]="eventsEmitter"
|
|
5927
|
+
* (RequestNewCalendarItemsEvent)="loadEvents($event)"
|
|
5928
|
+
* (CalendarItemClickedEvent)="onEventClick($event)"
|
|
5929
|
+
* (ButtonClickedEvent)="openModal()">
|
|
5930
|
+
* </app-calendar-view>
|
|
5931
|
+
* ```
|
|
5932
|
+
*/
|
|
5933
|
+
class CalendarViewComponent {
|
|
5934
|
+
/** Whether to show the action button in the toolbar. */
|
|
5935
|
+
showButton = false;
|
|
5936
|
+
/** Label text for the action button. */
|
|
5937
|
+
buttonTitle = '';
|
|
5938
|
+
/** Custom event renderer component type. */
|
|
5939
|
+
CalendarEventComponent;
|
|
5940
|
+
/** Observable or EventEmitter that pushes new event arrays into the calendar. */
|
|
5941
|
+
NewCalendarItemsEvent;
|
|
5942
|
+
/** Emits when the calendar needs fresh event data (e.g. after navigation). */
|
|
5943
|
+
RequestNewCalendarItemsEvent = new EventEmitter();
|
|
5944
|
+
/** Emits when a calendar event is clicked. */
|
|
5945
|
+
CalendarItemClickedEvent = new EventEmitter();
|
|
5946
|
+
/** Emits when the action button is clicked. */
|
|
5947
|
+
ButtonClickedEvent = new EventEmitter();
|
|
5948
|
+
CalendarView = CalendarView;
|
|
5949
|
+
currentView = CalendarView.WEEK;
|
|
5950
|
+
focusDay = new Date();
|
|
5951
|
+
dateInputValue = '';
|
|
5952
|
+
viewOptions = [];
|
|
5953
|
+
isMobileView = false;
|
|
5954
|
+
/** BehaviorSubject so late-subscribing child views receive the last emitted events. */
|
|
5955
|
+
internalEventsChanged = new BehaviorSubject([]);
|
|
5956
|
+
/** Subject for broadcasting focus-day changes to child views. */
|
|
5957
|
+
internalFocusDayChanged = new Subject();
|
|
5958
|
+
destroy$ = new Subject();
|
|
5959
|
+
formatter;
|
|
5960
|
+
config;
|
|
5961
|
+
destroyRef = inject(DestroyRef);
|
|
5962
|
+
lang = inject(MnLanguageService);
|
|
5963
|
+
constructor(formatter, mnConfig, legacyConfig) {
|
|
5964
|
+
this.formatter = formatter ?? new DefaultCalendarDateFormatter();
|
|
5965
|
+
// Priority: mn-config system > legacy CALENDAR_CONFIG > built-in defaults
|
|
5966
|
+
const raw = mnConfig ?? legacyConfig ?? undefined;
|
|
5967
|
+
this.config = resolveCalendarConfig(raw);
|
|
5968
|
+
}
|
|
5969
|
+
onResize() {
|
|
5970
|
+
this.checkMobileView();
|
|
5971
|
+
}
|
|
5972
|
+
ngOnInit() {
|
|
5973
|
+
this.rebuildFromConfig();
|
|
5974
|
+
// Re-resolve config when locale changes (supports $translate in mn-config).
|
|
5975
|
+
const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
|
|
5976
|
+
this.rebuildFromConfig();
|
|
5977
|
+
});
|
|
5978
|
+
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
5979
|
+
this.checkMobileView();
|
|
5980
|
+
this.updateDateInput();
|
|
5981
|
+
this.RequestNewCalendarItemsEvent.emit(this.focusDay);
|
|
5982
|
+
if (this.NewCalendarItemsEvent) {
|
|
5983
|
+
this.NewCalendarItemsEvent.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
5984
|
+
this.internalEventsChanged.next(events);
|
|
5985
|
+
});
|
|
5986
|
+
}
|
|
5987
|
+
}
|
|
5988
|
+
ngOnDestroy() {
|
|
5989
|
+
this.destroy$.next();
|
|
5990
|
+
this.destroy$.complete();
|
|
5991
|
+
}
|
|
5992
|
+
/** Switches the active view. On mobile, forces day view. */
|
|
5993
|
+
switchView(view) {
|
|
5994
|
+
if (this.isMobileView) {
|
|
5995
|
+
this.currentView = CalendarView.DAY;
|
|
5996
|
+
return;
|
|
5997
|
+
}
|
|
5998
|
+
this.currentView = view;
|
|
5999
|
+
}
|
|
6000
|
+
/** Navigates to the previous period (month / week / day). */
|
|
6001
|
+
navigatePrevious() {
|
|
6002
|
+
const d = new Date(this.focusDay);
|
|
6003
|
+
switch (this.currentView) {
|
|
6004
|
+
case CalendarView.MONTH:
|
|
6005
|
+
d.setMonth(d.getMonth() - 1);
|
|
6006
|
+
break;
|
|
6007
|
+
case CalendarView.WEEK:
|
|
6008
|
+
d.setDate(d.getDate() - 7);
|
|
6009
|
+
break;
|
|
6010
|
+
case CalendarView.DAY:
|
|
6011
|
+
d.setDate(d.getDate() - 1);
|
|
6012
|
+
break;
|
|
6013
|
+
}
|
|
6014
|
+
this.setFocusDay(d);
|
|
6015
|
+
}
|
|
6016
|
+
/** Navigates to the next period (month / week / day). */
|
|
6017
|
+
navigateNext() {
|
|
6018
|
+
const d = new Date(this.focusDay);
|
|
6019
|
+
switch (this.currentView) {
|
|
6020
|
+
case CalendarView.MONTH:
|
|
6021
|
+
d.setMonth(d.getMonth() + 1);
|
|
6022
|
+
break;
|
|
6023
|
+
case CalendarView.WEEK:
|
|
6024
|
+
d.setDate(d.getDate() + 7);
|
|
6025
|
+
break;
|
|
6026
|
+
case CalendarView.DAY:
|
|
6027
|
+
d.setDate(d.getDate() + 1);
|
|
6028
|
+
break;
|
|
6029
|
+
}
|
|
6030
|
+
this.setFocusDay(d);
|
|
6031
|
+
}
|
|
6032
|
+
/** Navigates to today. */
|
|
6033
|
+
goToToday() {
|
|
6034
|
+
this.setFocusDay(new Date());
|
|
6035
|
+
}
|
|
6036
|
+
/** Handles the date-picker input change. */
|
|
6037
|
+
onDateInputChange(event) {
|
|
6038
|
+
const value = event.target.value;
|
|
6039
|
+
if (value) {
|
|
6040
|
+
this.setFocusDay(new Date(value));
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
/** Handles a day click from the month view — switches to day view. */
|
|
6044
|
+
onMonthDayClick(date) {
|
|
6045
|
+
this.currentView = CalendarView.DAY;
|
|
6046
|
+
this.setFocusDay(date);
|
|
6047
|
+
}
|
|
6048
|
+
/** Forwards a child event click to the parent output. */
|
|
6049
|
+
onEventClick(event) {
|
|
6050
|
+
this.CalendarItemClickedEvent.emit(event);
|
|
6051
|
+
}
|
|
6052
|
+
/** trackBy for view option buttons. */
|
|
6053
|
+
trackByView(_index, item) {
|
|
6054
|
+
return item.value;
|
|
6055
|
+
}
|
|
6056
|
+
/** Rebuilds view options and labels from the current config. */
|
|
6057
|
+
rebuildFromConfig() {
|
|
6058
|
+
this.viewOptions = [
|
|
6059
|
+
{ value: CalendarView.MONTH, label: this.config.viewLabels['MONTH'] ?? 'Month' },
|
|
6060
|
+
{ value: CalendarView.WEEK, label: this.config.viewLabels['WEEK'] ?? 'Week' },
|
|
6061
|
+
{ value: CalendarView.DAY, label: this.config.viewLabels['DAY'] ?? 'Day' }
|
|
6062
|
+
];
|
|
6063
|
+
}
|
|
6064
|
+
checkMobileView() {
|
|
6065
|
+
const wasMobile = this.isMobileView;
|
|
6066
|
+
this.isMobileView = window.innerWidth < this.config.mobileBreakpoint;
|
|
6067
|
+
if (this.isMobileView && !wasMobile) {
|
|
6068
|
+
this.currentView = CalendarView.DAY;
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
6071
|
+
setFocusDay(date) {
|
|
6072
|
+
this.focusDay = date;
|
|
6073
|
+
this.updateDateInput();
|
|
6074
|
+
this.internalFocusDayChanged.next(date);
|
|
6075
|
+
this.RequestNewCalendarItemsEvent.emit(date);
|
|
6076
|
+
}
|
|
6077
|
+
updateDateInput() {
|
|
6078
|
+
this.dateInputValue = this.formatter.formatDateForFormControl(this.focusDay);
|
|
6079
|
+
}
|
|
6080
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }, { token: MN_CALENDAR_CONFIG, optional: true }, { token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
6081
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarViewComponent, isStandalone: true, selector: "app-calendar-view", inputs: { showButton: "showButton", buttonTitle: "buttonTitle", CalendarEventComponent: "CalendarEventComponent", NewCalendarItemsEvent: "NewCalendarItemsEvent" }, outputs: { RequestNewCalendarItemsEvent: "RequestNewCalendarItemsEvent", CalendarItemClickedEvent: "CalendarItemClickedEvent", ButtonClickedEvent: "ButtonClickedEvent" }, host: { listeners: { "window:resize": "onResize()" } }, providers: [
|
|
6082
|
+
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
6083
|
+
], ngImport: i0, template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">‹</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">›</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\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: "component", type: CalendarMonthComponent, selector: "app-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "app-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "app-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "app-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }] });
|
|
6084
|
+
}
|
|
6085
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
|
|
6086
|
+
type: Component,
|
|
6087
|
+
args: [{ selector: 'app-calendar-view', standalone: true, imports: [
|
|
6088
|
+
CommonModule,
|
|
6089
|
+
CalendarMonthComponent,
|
|
6090
|
+
CalendarWeekComponent,
|
|
6091
|
+
CalendarDayComponent,
|
|
6092
|
+
UpcomingEventsComponent
|
|
6093
|
+
], providers: [
|
|
6094
|
+
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
6095
|
+
], template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">‹</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">›</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\n"] }]
|
|
6096
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
6097
|
+
type: Optional
|
|
6098
|
+
}, {
|
|
6099
|
+
type: Inject,
|
|
6100
|
+
args: [CALENDAR_DATE_FORMATTER]
|
|
6101
|
+
}] }, { type: undefined, decorators: [{
|
|
6102
|
+
type: Optional
|
|
6103
|
+
}, {
|
|
6104
|
+
type: Inject,
|
|
6105
|
+
args: [MN_CALENDAR_CONFIG]
|
|
6106
|
+
}] }, { type: undefined, decorators: [{
|
|
6107
|
+
type: Optional
|
|
6108
|
+
}, {
|
|
6109
|
+
type: Inject,
|
|
6110
|
+
args: [CALENDAR_CONFIG]
|
|
6111
|
+
}] }], propDecorators: { showButton: [{
|
|
6112
|
+
type: Input
|
|
6113
|
+
}], buttonTitle: [{
|
|
6114
|
+
type: Input
|
|
6115
|
+
}], CalendarEventComponent: [{
|
|
6116
|
+
type: Input
|
|
6117
|
+
}], NewCalendarItemsEvent: [{
|
|
6118
|
+
type: Input
|
|
6119
|
+
}], RequestNewCalendarItemsEvent: [{
|
|
6120
|
+
type: Output
|
|
6121
|
+
}], CalendarItemClickedEvent: [{
|
|
6122
|
+
type: Output
|
|
6123
|
+
}], ButtonClickedEvent: [{
|
|
6124
|
+
type: Output
|
|
6125
|
+
}], onResize: [{
|
|
6126
|
+
type: HostListener,
|
|
6127
|
+
args: ['window:resize']
|
|
6128
|
+
}] } });
|
|
6129
|
+
|
|
6130
|
+
// Main component
|
|
6131
|
+
|
|
4311
6132
|
class MnSectionDirective {
|
|
4312
6133
|
/** Section name contributed by this DOM node to the section path */
|
|
4313
6134
|
mnSection;
|
|
@@ -4797,6 +6618,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4797
6618
|
}]
|
|
4798
6619
|
}], ctorParameters: () => [{ type: MnLanguageService }] });
|
|
4799
6620
|
|
|
6621
|
+
/**
|
|
6622
|
+
* Enable live preview mode. Listens for postMessage events from
|
|
6623
|
+
* Mn Web Manager and hot-swaps config/translations at runtime.
|
|
6624
|
+
*
|
|
6625
|
+
* Call this once in your app's bootstrap (e.g., APP_INITIALIZER or root component).
|
|
6626
|
+
*
|
|
6627
|
+
* @param configService - The MnConfigService instance
|
|
6628
|
+
* @param langService - The MnLanguageService instance
|
|
6629
|
+
* @param allowedOrigins - Optional whitelist of allowed origins (security)
|
|
6630
|
+
*/
|
|
6631
|
+
function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
6632
|
+
window.addEventListener('message', async (event) => {
|
|
6633
|
+
if (allowedOrigins?.length && !allowedOrigins.includes(event.origin)) {
|
|
6634
|
+
return;
|
|
6635
|
+
}
|
|
6636
|
+
const data = event.data;
|
|
6637
|
+
if (!data?.type)
|
|
6638
|
+
return;
|
|
6639
|
+
switch (data.type) {
|
|
6640
|
+
case 'mn-config-update':
|
|
6641
|
+
if (data.config) {
|
|
6642
|
+
await configService.loadFromObject(data.config);
|
|
6643
|
+
}
|
|
6644
|
+
break;
|
|
6645
|
+
case 'mn-translations-update':
|
|
6646
|
+
if (data.translations) {
|
|
6647
|
+
for (const [locale, translations] of Object.entries(data.translations)) {
|
|
6648
|
+
langService.registerTranslations(locale, translations);
|
|
6649
|
+
}
|
|
6650
|
+
await langService.setLocale(langService.locale);
|
|
6651
|
+
}
|
|
6652
|
+
break;
|
|
6653
|
+
}
|
|
6654
|
+
});
|
|
6655
|
+
}
|
|
6656
|
+
|
|
4800
6657
|
/*
|
|
4801
6658
|
* Public API Surface of mn-lib
|
|
4802
6659
|
*/
|
|
@@ -4805,5 +6662,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
4805
6662
|
* Generated bundle index. Do not edit.
|
|
4806
6663
|
*/
|
|
4807
6664
|
|
|
4808
|
-
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_MN_ALERT_CONFIG, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig, provideMnLanguage };
|
|
6665
|
+
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_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, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
4809
6666
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|