@maro-tech/form 0.0.16 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/fesm2022/maro-tech-form.mjs +1414 -1135
- package/fesm2022/maro-tech-form.mjs.map +1 -1
- package/package.json +1 -1
- package/types/maro-tech-form.d.ts +105 -110
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, inject,
|
|
2
|
+
import { signal, Injectable, inject, input, output, ViewChild, Component, EventEmitter, forwardRef, Output, Input, ChangeDetectorRef, ChangeDetectionStrategy, HostListener, DestroyRef, ElementRef, booleanAttribute, ViewContainerRef, computed } from '@angular/core';
|
|
3
3
|
import { HttpClient } from '@angular/common/http';
|
|
4
4
|
import * as i1$2 from '@angular/common';
|
|
5
5
|
import { CommonModule } from '@angular/common';
|
|
6
6
|
import * as i2 from '@angular/forms';
|
|
7
|
-
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
8
|
-
import {
|
|
7
|
+
import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
8
|
+
import { UiButtonComponent, UiToastService } from '@maro-tech/core';
|
|
9
9
|
import { BrowserMultiFormatOneDReader } from '@zxing/browser';
|
|
10
10
|
import { DecodeHintType, BarcodeFormat } from '@zxing/library';
|
|
11
|
-
import { UiButtonComponent } from '@maro-tech/core';
|
|
12
|
-
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
|
13
11
|
import * as i1 from 'lucide-angular';
|
|
14
|
-
import {
|
|
12
|
+
import { CalendarDays, ChevronLeft, ChevronRight, LucideAngularModule, Eye, EyeOff, X, ChevronDown, Check } from 'lucide-angular';
|
|
15
13
|
import * as i1$1 from '@angular/cdk/drag-drop';
|
|
16
14
|
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
|
|
15
|
+
import { forkJoin, map, Subject, debounceTime, distinctUntilChanged, of } from 'rxjs';
|
|
16
|
+
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
|
17
17
|
import { Router } from '@angular/router';
|
|
18
18
|
|
|
19
19
|
class UiFormContext {
|
|
@@ -79,8 +79,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
79
79
|
type: Injectable
|
|
80
80
|
}] });
|
|
81
81
|
|
|
82
|
-
const UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS = new InjectionToken('UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS');
|
|
83
|
-
|
|
84
82
|
const ACTION_PRINT = 'ACTION_PRINT';
|
|
85
83
|
const ACTION_FORM = 'ACTION_FORM';
|
|
86
84
|
const ACTION_API = 'ACTION_API';
|
|
@@ -101,7 +99,7 @@ const flattenDynamicFormFields = (fields) => {
|
|
|
101
99
|
return result;
|
|
102
100
|
};
|
|
103
101
|
|
|
104
|
-
class
|
|
102
|
+
class InputBarcodeComponent {
|
|
105
103
|
value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
106
104
|
label = input('Штрихкод', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
107
105
|
placeholder = input('Відскануйте або введіть штрихкод', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
@@ -365,8 +363,8 @@ class UiBarcodeScannerInputComponent {
|
|
|
365
363
|
return null;
|
|
366
364
|
}
|
|
367
365
|
}
|
|
368
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
369
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
366
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputBarcodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
367
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputBarcodeComponent, isStandalone: true, selector: "input-barcode", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoOpen: { classPropertyName: "autoOpen", publicName: "autoOpen", isSignal: true, isRequired: false, transformFunction: null }, searchButtonLabel: { classPropertyName: "searchButtonLabel", publicName: "searchButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, openCameraLabel: { classPropertyName: "openCameraLabel", publicName: "openCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, closeCameraLabel: { classPropertyName: "closeCameraLabel", publicName: "closeCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraTitle: { classPropertyName: "cameraTitle", publicName: "cameraTitle", isSignal: true, isRequired: false, transformFunction: null }, cameraStartingLabel: { classPropertyName: "cameraStartingLabel", publicName: "cameraStartingLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraHint: { classPropertyName: "cameraHint", publicName: "cameraHint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", scanned: "scanned", searchRequested: "searchRequested", cameraStateChange: "cameraStateChange" }, viewQueries: [{ propertyName: "cameraVideo", first: true, predicate: ["cameraVideo"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
370
368
|
<div class="space-y-2">
|
|
371
369
|
@if (label()) {
|
|
372
370
|
<label class="flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500" for="barcode-scanner-input">
|
|
@@ -460,10 +458,10 @@ class UiBarcodeScannerInputComponent {
|
|
|
460
458
|
</div>
|
|
461
459
|
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }] });
|
|
462
460
|
}
|
|
463
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
461
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputBarcodeComponent, decorators: [{
|
|
464
462
|
type: Component,
|
|
465
463
|
args: [{
|
|
466
|
-
selector: '
|
|
464
|
+
selector: 'input-barcode',
|
|
467
465
|
standalone: true,
|
|
468
466
|
imports: [CommonModule, FormsModule, UiButtonComponent],
|
|
469
467
|
template: `
|
|
@@ -565,6 +563,154 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
565
563
|
args: ['cameraVideo']
|
|
566
564
|
}] } });
|
|
567
565
|
|
|
566
|
+
class InputCheckboxComponent {
|
|
567
|
+
label;
|
|
568
|
+
disabled = false;
|
|
569
|
+
value = false;
|
|
570
|
+
valueChange = new EventEmitter();
|
|
571
|
+
onChange = () => undefined;
|
|
572
|
+
onTouched = () => undefined;
|
|
573
|
+
writeValue(value) { this.value = !!value; }
|
|
574
|
+
registerOnChange(fn) { this.onChange = fn; }
|
|
575
|
+
registerOnTouched(fn) { this.onTouched = fn; }
|
|
576
|
+
setDisabledState(isDisabled) { this.disabled = isDisabled; }
|
|
577
|
+
onValueChange(event) {
|
|
578
|
+
const value = event.target.checked;
|
|
579
|
+
this.value = value;
|
|
580
|
+
this.onChange(value);
|
|
581
|
+
this.valueChange.emit(value);
|
|
582
|
+
}
|
|
583
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputCheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
584
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: InputCheckboxComponent, isStandalone: true, selector: "input-checkbox", inputs: { label: "label", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputCheckboxComponent), multi: true }], ngImport: i0, template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span>{{ label }}</span>\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
585
|
+
}
|
|
586
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputCheckboxComponent, decorators: [{
|
|
587
|
+
type: Component,
|
|
588
|
+
args: [{ selector: 'input-checkbox', standalone: true, imports: [CommonModule], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputCheckboxComponent), multi: true }], template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span>{{ label }}</span>\n</label>\n" }]
|
|
589
|
+
}], propDecorators: { label: [{
|
|
590
|
+
type: Input
|
|
591
|
+
}], disabled: [{
|
|
592
|
+
type: Input
|
|
593
|
+
}], value: [{
|
|
594
|
+
type: Input
|
|
595
|
+
}], valueChange: [{
|
|
596
|
+
type: Output
|
|
597
|
+
}] } });
|
|
598
|
+
|
|
599
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
600
|
+
const validateFormControl = (control, required, pattern, email = false) => {
|
|
601
|
+
const value = control.value;
|
|
602
|
+
const empty = value == null || (typeof value === 'string' && value.trim() === '') || (Array.isArray(value) && value.length === 0);
|
|
603
|
+
if (required && empty)
|
|
604
|
+
return { required: true };
|
|
605
|
+
if (empty)
|
|
606
|
+
return null;
|
|
607
|
+
if (email && (typeof value !== 'string' || !EMAIL_PATTERN.test(value.trim())))
|
|
608
|
+
return { email: true };
|
|
609
|
+
if (pattern) {
|
|
610
|
+
try {
|
|
611
|
+
if (!new RegExp(pattern).test(String(value)))
|
|
612
|
+
return { pattern: true };
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
class InputColorComponent {
|
|
622
|
+
cdr = inject(ChangeDetectorRef);
|
|
623
|
+
label;
|
|
624
|
+
hint;
|
|
625
|
+
name;
|
|
626
|
+
id;
|
|
627
|
+
required = false;
|
|
628
|
+
fullWidth = false;
|
|
629
|
+
error;
|
|
630
|
+
disabled = false;
|
|
631
|
+
value = '#000000';
|
|
632
|
+
static nextId = 0;
|
|
633
|
+
generatedId = `input-color-${InputColorComponent.nextId++}`;
|
|
634
|
+
onChange = () => undefined;
|
|
635
|
+
onTouched = () => undefined;
|
|
636
|
+
get controlId() {
|
|
637
|
+
return this.id ?? this.generatedId;
|
|
638
|
+
}
|
|
639
|
+
get pickerValue() {
|
|
640
|
+
return this.isColorValue(this.value) ? this.value : '#000000';
|
|
641
|
+
}
|
|
642
|
+
writeValue(value) {
|
|
643
|
+
this.value = value?.trim() || '#000000';
|
|
644
|
+
this.cdr.markForCheck();
|
|
645
|
+
}
|
|
646
|
+
registerOnChange(fn) {
|
|
647
|
+
this.onChange = fn;
|
|
648
|
+
}
|
|
649
|
+
registerOnTouched(fn) {
|
|
650
|
+
this.onTouched = fn;
|
|
651
|
+
}
|
|
652
|
+
validate(control) { return validateFormControl(control, this.required); }
|
|
653
|
+
setDisabledState(isDisabled) {
|
|
654
|
+
this.disabled = isDisabled;
|
|
655
|
+
this.cdr.markForCheck();
|
|
656
|
+
}
|
|
657
|
+
onPickerChange(value) {
|
|
658
|
+
this.value = value;
|
|
659
|
+
this.onChange(value);
|
|
660
|
+
}
|
|
661
|
+
onHexInput(value) {
|
|
662
|
+
this.value = this.normalizeHexInput(value);
|
|
663
|
+
this.onChange(this.value);
|
|
664
|
+
}
|
|
665
|
+
markTouched() {
|
|
666
|
+
this.onTouched();
|
|
667
|
+
}
|
|
668
|
+
normalizeHexInput(value) {
|
|
669
|
+
const trimmed = value.trim();
|
|
670
|
+
if (!trimmed) {
|
|
671
|
+
return '';
|
|
672
|
+
}
|
|
673
|
+
return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
|
|
674
|
+
}
|
|
675
|
+
isColorValue(value) {
|
|
676
|
+
return /^#([0-9a-fA-F]{6})$/.test(value.trim());
|
|
677
|
+
}
|
|
678
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputColorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
679
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputColorComponent, isStandalone: true, selector: "input-color", inputs: { label: "label", hint: "hint", name: "name", id: "id", required: "required", fullWidth: "fullWidth", error: "error" }, providers: [
|
|
680
|
+
{
|
|
681
|
+
provide: NG_VALUE_ACCESSOR,
|
|
682
|
+
useExisting: forwardRef(() => InputColorComponent),
|
|
683
|
+
multi: true,
|
|
684
|
+
},
|
|
685
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputColorComponent), multi: true },
|
|
686
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200 disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
687
|
+
}
|
|
688
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputColorComponent, decorators: [{
|
|
689
|
+
type: Component,
|
|
690
|
+
args: [{ selector: 'input-color', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
691
|
+
{
|
|
692
|
+
provide: NG_VALUE_ACCESSOR,
|
|
693
|
+
useExisting: forwardRef(() => InputColorComponent),
|
|
694
|
+
multi: true,
|
|
695
|
+
},
|
|
696
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputColorComponent), multi: true },
|
|
697
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200 disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n" }]
|
|
698
|
+
}], propDecorators: { label: [{
|
|
699
|
+
type: Input
|
|
700
|
+
}], hint: [{
|
|
701
|
+
type: Input
|
|
702
|
+
}], name: [{
|
|
703
|
+
type: Input
|
|
704
|
+
}], id: [{
|
|
705
|
+
type: Input
|
|
706
|
+
}], required: [{
|
|
707
|
+
type: Input
|
|
708
|
+
}], fullWidth: [{
|
|
709
|
+
type: Input
|
|
710
|
+
}], error: [{
|
|
711
|
+
type: Input
|
|
712
|
+
}] } });
|
|
713
|
+
|
|
568
714
|
class UiPopupService {
|
|
569
715
|
activeClose = null;
|
|
570
716
|
open(close) {
|
|
@@ -587,419 +733,285 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
587
733
|
args: [{ providedIn: 'root' }]
|
|
588
734
|
}] });
|
|
589
735
|
|
|
590
|
-
class
|
|
591
|
-
closeIcon = X;
|
|
592
|
-
chevronDownIcon = ChevronDown;
|
|
593
|
-
checkIcon = Check;
|
|
736
|
+
class InputDateComponent {
|
|
594
737
|
cdr = inject(ChangeDetectorRef);
|
|
595
|
-
destroyRef = inject(DestroyRef);
|
|
596
|
-
http = inject(HttpClient);
|
|
597
738
|
popupService = inject(UiPopupService);
|
|
598
|
-
closeActivePopup = () => this.
|
|
739
|
+
closeActivePopup = () => this.closeCalendar();
|
|
599
740
|
label;
|
|
741
|
+
hint;
|
|
600
742
|
placeholder;
|
|
601
743
|
name;
|
|
602
|
-
|
|
744
|
+
id;
|
|
745
|
+
type = 'date';
|
|
746
|
+
element = 'input';
|
|
747
|
+
calendarIcon = CalendarDays;
|
|
748
|
+
previousMonthIcon = ChevronLeft;
|
|
749
|
+
nextMonthIcon = ChevronRight;
|
|
750
|
+
rows = 4;
|
|
603
751
|
required = false;
|
|
604
752
|
fullWidth = false;
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
includeBlankOption = false;
|
|
608
|
-
blankOptionLabel = 'Оберіть…';
|
|
609
|
-
emptyStateLabel = 'Немає варіантів для вибору';
|
|
610
|
-
searchable = true;
|
|
611
|
-
remoteSearch = false;
|
|
612
|
-
searchPlaceholder = 'Пошук...';
|
|
613
|
-
_options = [];
|
|
614
|
-
set options(value) {
|
|
615
|
-
this._options = value ?? [];
|
|
616
|
-
this.updateSelectionFromValue();
|
|
617
|
-
this.cdr.markForCheck();
|
|
618
|
-
}
|
|
619
|
-
get options() {
|
|
620
|
-
return this._options;
|
|
621
|
-
}
|
|
753
|
+
autocomplete = 'off';
|
|
754
|
+
pattern;
|
|
622
755
|
error;
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
panelClass = '';
|
|
626
|
-
valueChange = new EventEmitter();
|
|
627
|
-
searchChange = new EventEmitter();
|
|
628
|
-
popupMinWidth = 300;
|
|
629
|
-
popupViewportMargin = 16;
|
|
630
|
-
popupOffset = 8;
|
|
631
|
-
dropdownTrigger;
|
|
632
|
-
dropdownPositions = [
|
|
633
|
-
{
|
|
634
|
-
originX: 'start',
|
|
635
|
-
originY: 'bottom',
|
|
636
|
-
overlayX: 'start',
|
|
637
|
-
overlayY: 'top',
|
|
638
|
-
offsetY: this.popupOffset,
|
|
639
|
-
},
|
|
640
|
-
{
|
|
641
|
-
originX: 'start',
|
|
642
|
-
originY: 'top',
|
|
643
|
-
overlayX: 'start',
|
|
644
|
-
overlayY: 'bottom',
|
|
645
|
-
offsetY: -this.popupOffset,
|
|
646
|
-
},
|
|
647
|
-
];
|
|
648
|
-
isOpen = false;
|
|
649
|
-
popupWidth = 0;
|
|
756
|
+
actions = [];
|
|
757
|
+
disabled = false;
|
|
650
758
|
value = '';
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
759
|
+
static nextId = 0;
|
|
760
|
+
generatedId = `input-date-${InputDateComponent.nextId++}`;
|
|
761
|
+
visibleMonth = this.startOfMonth(new Date());
|
|
762
|
+
calendarView = 'days';
|
|
763
|
+
isCalendarOpen = false;
|
|
764
|
+
actionTriggered = new EventEmitter();
|
|
765
|
+
get controlId() {
|
|
766
|
+
return this.id ?? this.generatedId;
|
|
767
|
+
}
|
|
658
768
|
onChange = () => undefined;
|
|
659
769
|
onTouched = () => undefined;
|
|
660
|
-
constructor() {
|
|
661
|
-
this.searchChanges$.pipe(debounceTime(300), distinctUntilChanged()).subscribe((next) => {
|
|
662
|
-
this.searchTerm = next;
|
|
663
|
-
if (this.getDictionaryName()) {
|
|
664
|
-
this.loadDictionaryOptions(next);
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
this.updateSelectionFromValue();
|
|
668
|
-
this.cdr.markForCheck();
|
|
669
|
-
});
|
|
670
|
-
this.destroyRef.onDestroy(() => {
|
|
671
|
-
this.searchChanges$.complete();
|
|
672
|
-
this.dictionaryRequestVersions.clear();
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
ngOnChanges(changes) {
|
|
676
|
-
if ('data' in changes) {
|
|
677
|
-
this.reloadDictionaryOptions();
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
770
|
writeValue(value) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
this.value = value ?? '';
|
|
771
|
+
this.value = value ?? '';
|
|
772
|
+
const selectedDate = this.parseDate(this.value);
|
|
773
|
+
if (selectedDate) {
|
|
774
|
+
this.visibleMonth = this.startOfMonth(selectedDate);
|
|
686
775
|
}
|
|
687
|
-
this.updateSelectionFromValue();
|
|
688
|
-
this.ensureSelectedDictionaryOptionsLoaded();
|
|
689
776
|
this.cdr.markForCheck();
|
|
690
777
|
}
|
|
691
|
-
|
|
692
|
-
this.
|
|
778
|
+
get monthLabel() {
|
|
779
|
+
return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long', year: 'numeric' });
|
|
693
780
|
}
|
|
694
|
-
|
|
695
|
-
this.
|
|
781
|
+
get monthName() {
|
|
782
|
+
return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' });
|
|
696
783
|
}
|
|
697
|
-
|
|
698
|
-
this.
|
|
699
|
-
this.cdr.markForCheck();
|
|
784
|
+
get yearLabel() {
|
|
785
|
+
return this.visibleMonth.getFullYear();
|
|
700
786
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
787
|
+
get monthOptions() {
|
|
788
|
+
return Array.from({ length: 12 }, (_, value) => ({
|
|
789
|
+
value,
|
|
790
|
+
label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }),
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
get yearOptions() {
|
|
794
|
+
const year = this.visibleMonth.getFullYear();
|
|
795
|
+
return Array.from({ length: 12 }, (_, index) => year - 5 + index);
|
|
796
|
+
}
|
|
797
|
+
get calendarDays() {
|
|
798
|
+
const firstDay = this.startOfMonth(this.visibleMonth);
|
|
799
|
+
const mondayOffset = (firstDay.getDay() + 6) % 7;
|
|
800
|
+
const firstGridDay = new Date(firstDay);
|
|
801
|
+
firstGridDay.setDate(firstGridDay.getDate() - mondayOffset);
|
|
802
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
803
|
+
const date = new Date(firstGridDay);
|
|
804
|
+
date.setDate(firstGridDay.getDate() + index);
|
|
805
|
+
return {
|
|
806
|
+
date: this.toDateValue(date),
|
|
807
|
+
day: date.getDate(),
|
|
808
|
+
currentMonth: date.getMonth() === this.visibleMonth.getMonth(),
|
|
809
|
+
};
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
get displayValue() {
|
|
813
|
+
const date = this.selectedDate;
|
|
814
|
+
return date ? date.toLocaleDateString('uk-UA') : String(this.value ?? '');
|
|
815
|
+
}
|
|
816
|
+
get selectedDate() {
|
|
817
|
+
return this.parseDate(this.value);
|
|
818
|
+
}
|
|
819
|
+
openCalendar(event) {
|
|
820
|
+
event.stopPropagation();
|
|
821
|
+
if (this.disabled) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
this.isCalendarOpen = true;
|
|
825
|
+
this.popupService.open(this.closeActivePopup);
|
|
826
|
+
this.calendarView = 'days';
|
|
716
827
|
this.cdr.markForCheck();
|
|
717
828
|
}
|
|
718
|
-
|
|
829
|
+
closeCalendar() {
|
|
830
|
+
this.isCalendarOpen = false;
|
|
831
|
+
this.popupService.close(this.closeActivePopup);
|
|
832
|
+
this.cdr.markForCheck();
|
|
833
|
+
}
|
|
834
|
+
showMonthPicker(event) {
|
|
719
835
|
event.stopPropagation();
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
836
|
+
this.calendarView = 'months';
|
|
837
|
+
this.cdr.markForCheck();
|
|
838
|
+
}
|
|
839
|
+
showYearPicker(event) {
|
|
840
|
+
event.stopPropagation();
|
|
841
|
+
this.calendarView = 'years';
|
|
842
|
+
this.cdr.markForCheck();
|
|
843
|
+
}
|
|
844
|
+
selectMonth(month, event) {
|
|
845
|
+
event.stopPropagation();
|
|
846
|
+
this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1);
|
|
847
|
+
this.calendarView = 'days';
|
|
848
|
+
this.cdr.markForCheck();
|
|
849
|
+
}
|
|
850
|
+
selectYear(year, event) {
|
|
851
|
+
event.stopPropagation();
|
|
852
|
+
this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1);
|
|
853
|
+
this.calendarView = 'months';
|
|
854
|
+
this.cdr.markForCheck();
|
|
855
|
+
}
|
|
856
|
+
changeMonth(offset, event) {
|
|
857
|
+
event.stopPropagation();
|
|
858
|
+
if (this.calendarView === 'years') {
|
|
859
|
+
this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset * 12, this.visibleMonth.getMonth(), 1);
|
|
860
|
+
}
|
|
861
|
+
else if (this.calendarView === 'months') {
|
|
862
|
+
this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset, this.visibleMonth.getMonth(), 1);
|
|
728
863
|
}
|
|
729
864
|
else {
|
|
730
|
-
|
|
731
|
-
this.value = nextValue;
|
|
732
|
-
this.onChange(nextValue);
|
|
733
|
-
this.valueChange.emit(nextValue);
|
|
734
|
-
this.closeDropdown();
|
|
865
|
+
this.visibleMonth = new Date(this.visibleMonth.getFullYear(), this.visibleMonth.getMonth() + offset, 1);
|
|
735
866
|
}
|
|
736
|
-
this.
|
|
737
|
-
}
|
|
738
|
-
markTouched() {
|
|
739
|
-
this.onTouched();
|
|
867
|
+
this.cdr.markForCheck();
|
|
740
868
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
this.
|
|
869
|
+
selectDate(dateValue, event) {
|
|
870
|
+
event.stopPropagation();
|
|
871
|
+
this.visibleMonth = this.startOfMonth(this.parseDate(dateValue));
|
|
872
|
+
this.value = dateValue;
|
|
873
|
+
this.onChange(dateValue);
|
|
874
|
+
this.closeCalendar();
|
|
746
875
|
this.onTouched();
|
|
747
|
-
this.searchTerm = '';
|
|
748
|
-
this.searchChanges$.next('');
|
|
749
|
-
this.popupService.close(this.closeActivePopup);
|
|
750
876
|
this.cdr.markForCheck();
|
|
751
877
|
}
|
|
878
|
+
isSelected(dateValue) {
|
|
879
|
+
return this.value === dateValue;
|
|
880
|
+
}
|
|
881
|
+
isSelectedMonth(month) {
|
|
882
|
+
return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear();
|
|
883
|
+
}
|
|
884
|
+
isSelectedYear(year) {
|
|
885
|
+
return this.selectedDate?.getFullYear() === year;
|
|
886
|
+
}
|
|
752
887
|
onDocumentClick() {
|
|
753
|
-
if (this.
|
|
754
|
-
this.
|
|
888
|
+
if (this.isCalendarOpen) {
|
|
889
|
+
this.closeCalendar();
|
|
755
890
|
}
|
|
756
891
|
}
|
|
757
892
|
onEscape() {
|
|
758
|
-
if (this.
|
|
759
|
-
this.
|
|
893
|
+
if (this.isCalendarOpen) {
|
|
894
|
+
this.closeCalendar();
|
|
760
895
|
}
|
|
761
896
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (!this.selectedOptions.length) {
|
|
765
|
-
return this.placeholder ?? this.blankOptionLabel;
|
|
766
|
-
}
|
|
767
|
-
return `${this.selectedOptions.length}`;
|
|
768
|
-
}
|
|
769
|
-
return this.selectedOptions[0]?.label ?? this.placeholder ?? this.blankOptionLabel;
|
|
897
|
+
registerOnChange(fn) {
|
|
898
|
+
this.onChange = fn;
|
|
770
899
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
900
|
+
registerOnTouched(fn) {
|
|
901
|
+
this.onTouched = fn;
|
|
902
|
+
}
|
|
903
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern); }
|
|
904
|
+
setDisabledState(isDisabled) {
|
|
905
|
+
this.disabled = isDisabled;
|
|
906
|
+
this.cdr.markForCheck();
|
|
907
|
+
}
|
|
908
|
+
onValueChange(value) {
|
|
909
|
+
const maskedValue = this.applyDateMask(value);
|
|
910
|
+
const date = this.parseInputDate(maskedValue);
|
|
911
|
+
this.value = date ? this.toDateValue(date) : maskedValue;
|
|
912
|
+
this.onChange(this.value);
|
|
913
|
+
if (date) {
|
|
914
|
+
this.visibleMonth = this.startOfMonth(date);
|
|
775
915
|
}
|
|
776
|
-
|
|
916
|
+
this.cdr.markForCheck();
|
|
777
917
|
}
|
|
778
|
-
|
|
779
|
-
|
|
918
|
+
allowDateKey(event) {
|
|
919
|
+
if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
920
|
+
return;
|
|
921
|
+
if (!/^\d$/.test(event.key))
|
|
922
|
+
event.preventDefault();
|
|
780
923
|
}
|
|
781
|
-
|
|
782
|
-
|
|
924
|
+
applyDateMask(value) {
|
|
925
|
+
const digits = value.replace(/\D/g, '').slice(0, 8);
|
|
926
|
+
const day = digits.slice(0, 2);
|
|
927
|
+
const month = digits.slice(2, 4);
|
|
928
|
+
const year = digits.slice(4, 8);
|
|
929
|
+
return [day, month, year].filter(Boolean).join('.');
|
|
783
930
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (!this.searchable || this.remoteSearch || !this.searchTerm.trim()) {
|
|
787
|
-
return options;
|
|
788
|
-
}
|
|
789
|
-
const search = this.searchTerm.trim().toLowerCase();
|
|
790
|
-
return options.filter((option) => {
|
|
791
|
-
if (this.includeBlankOption && !this.multiple && this.isBlankOption(option)) {
|
|
792
|
-
return true;
|
|
793
|
-
}
|
|
794
|
-
return String(option.label ?? '').toLowerCase().includes(search);
|
|
795
|
-
});
|
|
931
|
+
triggerAction(action) {
|
|
932
|
+
this.actionTriggered.emit(action);
|
|
796
933
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
this.searchChanges$.next(next);
|
|
800
|
-
this.searchChange.emit(next);
|
|
934
|
+
startOfMonth(date) {
|
|
935
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
801
936
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
if (this.multiple) {
|
|
806
|
-
this.multiValue = this.multiValue.filter((v) => v !== key);
|
|
807
|
-
this.onChange(this.multiValue);
|
|
808
|
-
this.valueChange.emit(this.multiValue);
|
|
809
|
-
this.updateSelectionFromValue();
|
|
937
|
+
parseDate(value) {
|
|
938
|
+
if (!value) {
|
|
939
|
+
return null;
|
|
810
940
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (this.multiple) {
|
|
815
|
-
this.selectedOptions = options.filter((option) => this.multiValue.includes(this.getOptionValue(option)));
|
|
816
|
-
return;
|
|
941
|
+
const [year, month, day] = String(value).slice(0, 10).split('-').map(Number);
|
|
942
|
+
if (!year || !month || !day) {
|
|
943
|
+
return null;
|
|
817
944
|
}
|
|
818
|
-
|
|
819
|
-
this.selectedOptions = current ? [current] : [];
|
|
945
|
+
return new Date(year, month - 1, day);
|
|
820
946
|
}
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
947
|
+
parseInputDate(value) {
|
|
948
|
+
const normalized = value.trim();
|
|
949
|
+
const isoDate = this.parseDate(normalized);
|
|
950
|
+
if (isoDate) {
|
|
951
|
+
return isoDate;
|
|
825
952
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const merged = new Map();
|
|
830
|
-
for (const options of optionSets) {
|
|
831
|
-
for (const option of options || []) {
|
|
832
|
-
const value = this.getOptionValue(option);
|
|
833
|
-
const key = `${value}::${String(option.label || '').trim()}`;
|
|
834
|
-
if (!merged.has(key)) {
|
|
835
|
-
merged.set(key, option);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
953
|
+
const match = normalized.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
954
|
+
if (!match) {
|
|
955
|
+
return null;
|
|
838
956
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
957
|
+
const [, day, month, year] = match;
|
|
958
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
959
|
+
return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day)
|
|
960
|
+
? date
|
|
961
|
+
: null;
|
|
843
962
|
}
|
|
844
|
-
|
|
845
|
-
|
|
963
|
+
toDateValue(date) {
|
|
964
|
+
const year = date.getFullYear();
|
|
965
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
966
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
967
|
+
return `${year}-${month}-${day}`;
|
|
846
968
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
this.dictionaryRequestVersions.clear();
|
|
850
|
-
this.dictionaryOptions = [];
|
|
851
|
-
this.isDictionaryLoading = false;
|
|
852
|
-
if (!dictionaryName) {
|
|
853
|
-
this.updateSelectionFromValue();
|
|
854
|
-
this.cdr.markForCheck();
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
this.loadDictionaryOptions(this.searchTerm);
|
|
858
|
-
}
|
|
859
|
-
loadDictionaryOptions(query) {
|
|
860
|
-
const dictionaryName = this.getDictionaryName();
|
|
861
|
-
if (!dictionaryName) {
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
const requestVersion = (this.dictionaryRequestVersions.get(dictionaryName) || 0) + 1;
|
|
865
|
-
this.dictionaryRequestVersions.set(dictionaryName, requestVersion);
|
|
866
|
-
this.isDictionaryLoading = true;
|
|
867
|
-
this.loadSuggestItems(dictionaryName, String(query || '').trim()).subscribe({
|
|
868
|
-
next: (items) => {
|
|
869
|
-
if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
this.dictionaryOptions = this.normalizeSuggestItems(items);
|
|
873
|
-
this.isDictionaryLoading = false;
|
|
874
|
-
this.ensureSelectedDictionaryOptionsLoaded();
|
|
875
|
-
this.updateSelectionFromValue();
|
|
876
|
-
this.cdr.markForCheck();
|
|
877
|
-
},
|
|
878
|
-
error: () => {
|
|
879
|
-
if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
this.dictionaryOptions = [];
|
|
883
|
-
this.isDictionaryLoading = false;
|
|
884
|
-
this.updateSelectionFromValue();
|
|
885
|
-
this.cdr.markForCheck();
|
|
886
|
-
},
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
ensureSelectedDictionaryOptionsLoaded() {
|
|
890
|
-
const dictionaryName = this.getDictionaryName();
|
|
891
|
-
if (!dictionaryName) {
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
const existingValues = new Set(this.getRenderOptions().map((option) => this.getOptionValue(option)));
|
|
895
|
-
const selectedValues = this.multiple ? this.multiValue : [this.value];
|
|
896
|
-
const missingValues = selectedValues
|
|
897
|
-
.map((item) => String(item || '').trim())
|
|
898
|
-
.filter((item) => !!item && !existingValues.has(item));
|
|
899
|
-
if (!missingValues.length) {
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
forkJoin(missingValues.map((value) => this.loadSuggestItem(dictionaryName, value))).subscribe({
|
|
903
|
-
next: (items) => {
|
|
904
|
-
const nextOptions = [...this.dictionaryOptions];
|
|
905
|
-
for (const item of items) {
|
|
906
|
-
const normalized = this.normalizeSuggestItem(item);
|
|
907
|
-
if (!nextOptions.some((option) => this.getOptionValue(option) === normalized.value)) {
|
|
908
|
-
nextOptions.push(normalized);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
this.dictionaryOptions = nextOptions;
|
|
912
|
-
this.updateSelectionFromValue();
|
|
913
|
-
this.cdr.markForCheck();
|
|
914
|
-
},
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
normalizeSuggestItems(items) {
|
|
918
|
-
return (items || [])
|
|
919
|
-
.map((item) => this.normalizeSuggestItem(item))
|
|
920
|
-
.filter((item) => !!item.label || item.value !== null);
|
|
921
|
-
}
|
|
922
|
-
normalizeSuggestItem(item) {
|
|
923
|
-
return {
|
|
924
|
-
label: String(item?.label ?? ''),
|
|
925
|
-
value: item?.key != null ? String(item.key) : null,
|
|
926
|
-
imageUrl: item?.imageId ? `/api/files/${item.imageId}/preview?scale=25` : undefined,
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
loadSuggestItems(name, query) {
|
|
930
|
-
const params = new URLSearchParams();
|
|
931
|
-
if (query?.trim())
|
|
932
|
-
params.set('q', query.trim());
|
|
933
|
-
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
934
|
-
return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
|
|
935
|
-
}
|
|
936
|
-
loadSuggestItem(name, key) {
|
|
937
|
-
return this.http.get(`/api/suggest/${encodeURIComponent(name)}/${encodeURIComponent(key)}`);
|
|
938
|
-
}
|
|
939
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
940
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiSelectComponent, isStandalone: true, selector: "forms-select", inputs: { label: "label", placeholder: "placeholder", name: "name", data: "data", required: "required", fullWidth: "fullWidth", disabled: "disabled", multiple: ["multiple", "multiple", booleanAttribute], includeBlankOption: "includeBlankOption", blankOptionLabel: "blankOptionLabel", emptyStateLabel: "emptyStateLabel", searchable: "searchable", remoteSearch: "remoteSearch", searchPlaceholder: "searchPlaceholder", options: "options", error: "error", hint: "hint", triggerClass: "triggerClass", panelClass: "panelClass" }, outputs: { valueChange: "valueChange", searchChange: "searchChange" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
|
|
969
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
970
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputDateComponent, isStandalone: true, selector: "input-date", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
|
|
941
971
|
{
|
|
942
972
|
provide: NG_VALUE_ACCESSOR,
|
|
943
|
-
useExisting: forwardRef(() =>
|
|
973
|
+
useExisting: forwardRef(() => InputDateComponent),
|
|
944
974
|
multi: true,
|
|
945
975
|
},
|
|
946
|
-
|
|
976
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDateComponent), multi: true },
|
|
977
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
947
978
|
}
|
|
948
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
979
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDateComponent, decorators: [{
|
|
949
980
|
type: Component,
|
|
950
|
-
args: [{ selector: '
|
|
981
|
+
args: [{ selector: 'input-date', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
951
982
|
{
|
|
952
983
|
provide: NG_VALUE_ACCESSOR,
|
|
953
|
-
useExisting: forwardRef(() =>
|
|
984
|
+
useExisting: forwardRef(() => InputDateComponent),
|
|
954
985
|
multi: true,
|
|
955
986
|
},
|
|
956
|
-
|
|
957
|
-
}],
|
|
987
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDateComponent), multi: true },
|
|
988
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n" }]
|
|
989
|
+
}], propDecorators: { label: [{
|
|
990
|
+
type: Input
|
|
991
|
+
}], hint: [{
|
|
958
992
|
type: Input
|
|
959
993
|
}], placeholder: [{
|
|
960
994
|
type: Input
|
|
961
995
|
}], name: [{
|
|
962
996
|
type: Input
|
|
963
|
-
}],
|
|
997
|
+
}], id: [{
|
|
998
|
+
type: Input
|
|
999
|
+
}], rows: [{
|
|
964
1000
|
type: Input
|
|
965
1001
|
}], required: [{
|
|
966
1002
|
type: Input
|
|
967
1003
|
}], fullWidth: [{
|
|
968
1004
|
type: Input
|
|
969
|
-
}],
|
|
970
|
-
type: Input
|
|
971
|
-
}], multiple: [{
|
|
972
|
-
type: Input,
|
|
973
|
-
args: [{ transform: booleanAttribute }]
|
|
974
|
-
}], includeBlankOption: [{
|
|
975
|
-
type: Input
|
|
976
|
-
}], blankOptionLabel: [{
|
|
977
|
-
type: Input
|
|
978
|
-
}], emptyStateLabel: [{
|
|
979
|
-
type: Input
|
|
980
|
-
}], searchable: [{
|
|
981
|
-
type: Input
|
|
982
|
-
}], remoteSearch: [{
|
|
983
|
-
type: Input
|
|
984
|
-
}], searchPlaceholder: [{
|
|
1005
|
+
}], autocomplete: [{
|
|
985
1006
|
type: Input
|
|
986
|
-
}],
|
|
1007
|
+
}], pattern: [{
|
|
987
1008
|
type: Input
|
|
988
1009
|
}], error: [{
|
|
989
1010
|
type: Input
|
|
990
|
-
}],
|
|
991
|
-
type: Input
|
|
992
|
-
}], triggerClass: [{
|
|
993
|
-
type: Input
|
|
994
|
-
}], panelClass: [{
|
|
1011
|
+
}], actions: [{
|
|
995
1012
|
type: Input
|
|
996
|
-
}],
|
|
997
|
-
type: Output
|
|
998
|
-
}], searchChange: [{
|
|
1013
|
+
}], actionTriggered: [{
|
|
999
1014
|
type: Output
|
|
1000
|
-
}], dropdownTrigger: [{
|
|
1001
|
-
type: ViewChild,
|
|
1002
|
-
args: ['dropdownTrigger', { static: true, read: ElementRef }]
|
|
1003
1015
|
}], onDocumentClick: [{
|
|
1004
1016
|
type: HostListener,
|
|
1005
1017
|
args: ['document:click']
|
|
@@ -1008,7 +1020,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1008
1020
|
args: ['document:keydown.escape']
|
|
1009
1021
|
}] } });
|
|
1010
1022
|
|
|
1011
|
-
class
|
|
1023
|
+
class InputDatetimeComponent {
|
|
1012
1024
|
cdr = inject(ChangeDetectorRef);
|
|
1013
1025
|
popupService = inject(UiPopupService);
|
|
1014
1026
|
closeActivePopup = () => this.closeCalendar();
|
|
@@ -1017,364 +1029,77 @@ class UiDateInputComponent {
|
|
|
1017
1029
|
placeholder;
|
|
1018
1030
|
name;
|
|
1019
1031
|
id;
|
|
1020
|
-
type = 'date';
|
|
1021
|
-
element = 'input';
|
|
1022
|
-
calendarIcon = CalendarDays;
|
|
1023
|
-
previousMonthIcon = ChevronLeft;
|
|
1024
|
-
nextMonthIcon = ChevronRight;
|
|
1025
1032
|
rows = 4;
|
|
1026
1033
|
required = false;
|
|
1027
1034
|
fullWidth = false;
|
|
1028
1035
|
autocomplete = 'off';
|
|
1029
|
-
readonly = false;
|
|
1030
1036
|
pattern;
|
|
1031
1037
|
error;
|
|
1032
1038
|
actions = [];
|
|
1039
|
+
type = 'datetime-local';
|
|
1040
|
+
calendarIcon = CalendarDays;
|
|
1041
|
+
previousMonthIcon = ChevronLeft;
|
|
1042
|
+
nextMonthIcon = ChevronRight;
|
|
1033
1043
|
disabled = false;
|
|
1034
1044
|
value = '';
|
|
1035
|
-
static nextId = 0;
|
|
1036
|
-
generatedId = `ui-input-${UiDateInputComponent.nextId++}`;
|
|
1037
|
-
visibleMonth = this.startOfMonth(new Date());
|
|
1038
|
-
calendarView = 'days';
|
|
1039
1045
|
isCalendarOpen = false;
|
|
1046
|
+
calendarView = 'days';
|
|
1047
|
+
visibleMonth = this.startOfMonth(new Date());
|
|
1048
|
+
selectedHour = 12;
|
|
1049
|
+
selectedMinute = 0;
|
|
1050
|
+
static nextId = 0;
|
|
1051
|
+
generatedId = `input-datetime-${InputDatetimeComponent.nextId++}`;
|
|
1040
1052
|
actionTriggered = new EventEmitter();
|
|
1041
|
-
get controlId() {
|
|
1042
|
-
return this.id ?? this.generatedId;
|
|
1043
|
-
}
|
|
1044
1053
|
onChange = () => undefined;
|
|
1045
1054
|
onTouched = () => undefined;
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
}
|
|
1054
|
-
get monthLabel() {
|
|
1055
|
-
return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long', year: 'numeric' });
|
|
1056
|
-
}
|
|
1057
|
-
get monthName() {
|
|
1058
|
-
return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' });
|
|
1059
|
-
}
|
|
1060
|
-
get yearLabel() {
|
|
1061
|
-
return this.visibleMonth.getFullYear();
|
|
1062
|
-
}
|
|
1063
|
-
get monthOptions() {
|
|
1064
|
-
return Array.from({ length: 12 }, (_, value) => ({
|
|
1065
|
-
value,
|
|
1066
|
-
label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }),
|
|
1067
|
-
}));
|
|
1068
|
-
}
|
|
1069
|
-
get yearOptions() {
|
|
1070
|
-
const year = this.visibleMonth.getFullYear();
|
|
1071
|
-
return Array.from({ length: 12 }, (_, index) => year - 5 + index);
|
|
1072
|
-
}
|
|
1055
|
+
get controlId() { return this.id ?? this.generatedId; }
|
|
1056
|
+
get selectedDate() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? new Date(+match[1], +match[2] - 1, +match[3]) : null; }
|
|
1057
|
+
get displayValue() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? `${match[3]}.${match[2]}.${match[1]} ${match[4]}:${match[5]}` : String(this.value ?? ''); }
|
|
1058
|
+
get monthName() { return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' }); }
|
|
1059
|
+
get yearLabel() { return this.visibleMonth.getFullYear(); }
|
|
1060
|
+
get monthOptions() { return Array.from({ length: 12 }, (_, value) => ({ value, label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }) })); }
|
|
1061
|
+
get yearOptions() { const year = this.visibleMonth.getFullYear(); return Array.from({ length: 12 }, (_, index) => year - 5 + index); }
|
|
1073
1062
|
get calendarDays() {
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
return Array.from({ length: 42 }, (_, index) => {
|
|
1079
|
-
const date = new Date(firstGridDay);
|
|
1080
|
-
date.setDate(firstGridDay.getDate() + index);
|
|
1081
|
-
return {
|
|
1082
|
-
date: this.toDateValue(date),
|
|
1083
|
-
day: date.getDate(),
|
|
1084
|
-
currentMonth: date.getMonth() === this.visibleMonth.getMonth(),
|
|
1085
|
-
};
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
get displayValue() {
|
|
1089
|
-
const date = this.selectedDate;
|
|
1090
|
-
return date ? date.toLocaleDateString('uk-UA') : String(this.value ?? '');
|
|
1091
|
-
}
|
|
1092
|
-
get selectedDate() {
|
|
1093
|
-
return this.parseDate(this.value);
|
|
1063
|
+
const first = this.startOfMonth(this.visibleMonth);
|
|
1064
|
+
const offset = (first.getDay() + 6) % 7;
|
|
1065
|
+
const start = new Date(first);
|
|
1066
|
+
start.setDate(start.getDate() - offset);
|
|
1067
|
+
return Array.from({ length: 42 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); return { date: this.toDateValue(date), day: date.getDate(), currentMonth: date.getMonth() === this.visibleMonth.getMonth() }; });
|
|
1094
1068
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1069
|
+
writeValue(value) { this.value = value ?? ''; const date = this.selectedDate; if (date) {
|
|
1070
|
+
this.visibleMonth = this.startOfMonth(date);
|
|
1071
|
+
this.selectedHour = +(String(value).slice(11, 13));
|
|
1072
|
+
this.selectedMinute = +(String(value).slice(14, 16));
|
|
1073
|
+
} this.cdr.markForCheck(); }
|
|
1074
|
+
registerOnChange(fn) { this.onChange = fn; }
|
|
1075
|
+
registerOnTouched(fn) { this.onTouched = fn; }
|
|
1076
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern); }
|
|
1077
|
+
setDisabledState(disabled) { this.disabled = disabled; this.cdr.markForCheck(); }
|
|
1078
|
+
openCalendar(event) { event.stopPropagation(); if (this.disabled)
|
|
1079
|
+
return; this.isCalendarOpen = true; this.calendarView = 'days'; this.popupService.open(this.closeActivePopup); this.cdr.markForCheck(); }
|
|
1080
|
+
closeCalendar() { this.isCalendarOpen = false; this.popupService.close(this.closeActivePopup); this.cdr.markForCheck(); }
|
|
1081
|
+
changeMonth(offset, event) { event.stopPropagation(); const year = this.visibleMonth.getFullYear(); const month = this.visibleMonth.getMonth(); this.visibleMonth = this.calendarView === 'years' ? new Date(year + offset * 12, month, 1) : new Date(year + (this.calendarView === 'months' ? offset : 0), month + (this.calendarView === 'days' ? offset : 0), 1); this.cdr.markForCheck(); }
|
|
1082
|
+
showMonthPicker(event) { event.stopPropagation(); this.calendarView = 'months'; this.cdr.markForCheck(); }
|
|
1083
|
+
showYearPicker(event) { event.stopPropagation(); this.calendarView = 'years'; this.cdr.markForCheck(); }
|
|
1084
|
+
selectMonth(month, event) { event.stopPropagation(); this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1); this.calendarView = 'days'; this.cdr.markForCheck(); }
|
|
1085
|
+
selectYear(year, event) { event.stopPropagation(); this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1); this.calendarView = 'months'; this.cdr.markForCheck(); }
|
|
1086
|
+
selectDate(dateValue, event) { event.stopPropagation(); const date = this.parseDate(dateValue); this.visibleMonth = this.startOfMonth(date); this.setDateTime(dateValue); }
|
|
1087
|
+
setDateTime(dateValue, close = true) { const hour = String(this.selectedHour).padStart(2, '0'); const minute = String(this.selectedMinute).padStart(2, '0'); this.value = `${dateValue}T${hour}:${minute}`; this.onChange(this.value); if (close)
|
|
1088
|
+
this.closeCalendar(); if (close)
|
|
1089
|
+
this.onTouched(); this.cdr.markForCheck(); }
|
|
1090
|
+
onTimeChange() { const date = this.selectedDate; if (date) {
|
|
1091
|
+
this.setDateTime(this.toDateValue(date), false);
|
|
1092
|
+
} }
|
|
1093
|
+
onValueChange(input) { const masked = this.applyMask(input); const match = masked.match(/^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/); if (match) {
|
|
1094
|
+
const date = new Date(+match[3], +match[2] - 1, +match[1]);
|
|
1095
|
+
if (date.getFullYear() === +match[3] && date.getMonth() === +match[2] - 1 && date.getDate() === +match[1] && +match[4] < 24 && +match[5] < 60) {
|
|
1096
|
+
this.value = `${match[3]}-${match[2]}-${match[1]}T${match[4]}:${match[5]}`;
|
|
1097
|
+
this.visibleMonth = this.startOfMonth(date);
|
|
1098
|
+
this.selectedHour = +match[4];
|
|
1099
|
+
this.selectedMinute = +match[5];
|
|
1099
1100
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
this.calendarView = 'days';
|
|
1103
|
-
this.cdr.markForCheck();
|
|
1104
|
-
}
|
|
1105
|
-
closeCalendar() {
|
|
1106
|
-
this.isCalendarOpen = false;
|
|
1107
|
-
this.popupService.close(this.closeActivePopup);
|
|
1108
|
-
this.cdr.markForCheck();
|
|
1109
|
-
}
|
|
1110
|
-
showMonthPicker(event) {
|
|
1111
|
-
event.stopPropagation();
|
|
1112
|
-
this.calendarView = 'months';
|
|
1113
|
-
this.cdr.markForCheck();
|
|
1114
|
-
}
|
|
1115
|
-
showYearPicker(event) {
|
|
1116
|
-
event.stopPropagation();
|
|
1117
|
-
this.calendarView = 'years';
|
|
1118
|
-
this.cdr.markForCheck();
|
|
1119
|
-
}
|
|
1120
|
-
selectMonth(month, event) {
|
|
1121
|
-
event.stopPropagation();
|
|
1122
|
-
this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1);
|
|
1123
|
-
this.calendarView = 'days';
|
|
1124
|
-
this.cdr.markForCheck();
|
|
1125
|
-
}
|
|
1126
|
-
selectYear(year, event) {
|
|
1127
|
-
event.stopPropagation();
|
|
1128
|
-
this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1);
|
|
1129
|
-
this.calendarView = 'months';
|
|
1130
|
-
this.cdr.markForCheck();
|
|
1131
|
-
}
|
|
1132
|
-
changeMonth(offset, event) {
|
|
1133
|
-
event.stopPropagation();
|
|
1134
|
-
if (this.calendarView === 'years') {
|
|
1135
|
-
this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset * 12, this.visibleMonth.getMonth(), 1);
|
|
1136
|
-
}
|
|
1137
|
-
else if (this.calendarView === 'months') {
|
|
1138
|
-
this.visibleMonth = new Date(this.visibleMonth.getFullYear() + offset, this.visibleMonth.getMonth(), 1);
|
|
1139
|
-
}
|
|
1140
|
-
else {
|
|
1141
|
-
this.visibleMonth = new Date(this.visibleMonth.getFullYear(), this.visibleMonth.getMonth() + offset, 1);
|
|
1142
|
-
}
|
|
1143
|
-
this.cdr.markForCheck();
|
|
1144
|
-
}
|
|
1145
|
-
selectDate(dateValue, event) {
|
|
1146
|
-
event.stopPropagation();
|
|
1147
|
-
this.visibleMonth = this.startOfMonth(this.parseDate(dateValue));
|
|
1148
|
-
this.value = dateValue;
|
|
1149
|
-
this.onChange(dateValue);
|
|
1150
|
-
this.closeCalendar();
|
|
1151
|
-
this.onTouched();
|
|
1152
|
-
this.cdr.markForCheck();
|
|
1153
|
-
}
|
|
1154
|
-
isSelected(dateValue) {
|
|
1155
|
-
return this.value === dateValue;
|
|
1156
|
-
}
|
|
1157
|
-
isSelectedMonth(month) {
|
|
1158
|
-
return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear();
|
|
1159
|
-
}
|
|
1160
|
-
isSelectedYear(year) {
|
|
1161
|
-
return this.selectedDate?.getFullYear() === year;
|
|
1162
|
-
}
|
|
1163
|
-
onDocumentClick() {
|
|
1164
|
-
if (this.isCalendarOpen) {
|
|
1165
|
-
this.closeCalendar();
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
onEscape() {
|
|
1169
|
-
if (this.isCalendarOpen) {
|
|
1170
|
-
this.closeCalendar();
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
registerOnChange(fn) {
|
|
1174
|
-
this.onChange = fn;
|
|
1175
|
-
}
|
|
1176
|
-
registerOnTouched(fn) {
|
|
1177
|
-
this.onTouched = fn;
|
|
1178
|
-
}
|
|
1179
|
-
setDisabledState(isDisabled) {
|
|
1180
|
-
this.disabled = isDisabled;
|
|
1181
|
-
this.cdr.markForCheck();
|
|
1182
|
-
}
|
|
1183
|
-
onValueChange(value) {
|
|
1184
|
-
const maskedValue = this.applyDateMask(value);
|
|
1185
|
-
const date = this.parseInputDate(maskedValue);
|
|
1186
|
-
this.value = date ? this.toDateValue(date) : maskedValue;
|
|
1187
|
-
this.onChange(this.value);
|
|
1188
|
-
if (date) {
|
|
1189
|
-
this.visibleMonth = this.startOfMonth(date);
|
|
1190
|
-
}
|
|
1191
|
-
this.cdr.markForCheck();
|
|
1192
|
-
}
|
|
1193
|
-
allowDateKey(event) {
|
|
1194
|
-
if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
1195
|
-
return;
|
|
1196
|
-
if (!/^\d$/.test(event.key))
|
|
1197
|
-
event.preventDefault();
|
|
1198
|
-
}
|
|
1199
|
-
applyDateMask(value) {
|
|
1200
|
-
const digits = value.replace(/\D/g, '').slice(0, 8);
|
|
1201
|
-
const day = digits.slice(0, 2);
|
|
1202
|
-
const month = digits.slice(2, 4);
|
|
1203
|
-
const year = digits.slice(4, 8);
|
|
1204
|
-
return [day, month, year].filter(Boolean).join('.');
|
|
1205
|
-
}
|
|
1206
|
-
triggerAction(action) {
|
|
1207
|
-
this.actionTriggered.emit(action);
|
|
1208
|
-
}
|
|
1209
|
-
startOfMonth(date) {
|
|
1210
|
-
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
1211
|
-
}
|
|
1212
|
-
parseDate(value) {
|
|
1213
|
-
if (!value) {
|
|
1214
|
-
return null;
|
|
1215
|
-
}
|
|
1216
|
-
const [year, month, day] = String(value).slice(0, 10).split('-').map(Number);
|
|
1217
|
-
if (!year || !month || !day) {
|
|
1218
|
-
return null;
|
|
1219
|
-
}
|
|
1220
|
-
return new Date(year, month - 1, day);
|
|
1221
|
-
}
|
|
1222
|
-
parseInputDate(value) {
|
|
1223
|
-
const normalized = value.trim();
|
|
1224
|
-
const isoDate = this.parseDate(normalized);
|
|
1225
|
-
if (isoDate) {
|
|
1226
|
-
return isoDate;
|
|
1227
|
-
}
|
|
1228
|
-
const match = normalized.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
1229
|
-
if (!match) {
|
|
1230
|
-
return null;
|
|
1231
|
-
}
|
|
1232
|
-
const [, day, month, year] = match;
|
|
1233
|
-
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
1234
|
-
return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day)
|
|
1235
|
-
? date
|
|
1236
|
-
: null;
|
|
1237
|
-
}
|
|
1238
|
-
toDateValue(date) {
|
|
1239
|
-
const year = date.getFullYear();
|
|
1240
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
1241
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
1242
|
-
return `${year}-${month}-${day}`;
|
|
1243
|
-
}
|
|
1244
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1245
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDateInputComponent, isStandalone: true, selector: "forms-date-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
|
|
1246
|
-
{
|
|
1247
|
-
provide: NG_VALUE_ACCESSOR,
|
|
1248
|
-
useExisting: forwardRef(() => UiDateInputComponent),
|
|
1249
|
-
multi: true,
|
|
1250
|
-
},
|
|
1251
|
-
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1252
|
-
}
|
|
1253
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, decorators: [{
|
|
1254
|
-
type: Component,
|
|
1255
|
-
args: [{ selector: 'forms-date-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1256
|
-
{
|
|
1257
|
-
provide: NG_VALUE_ACCESSOR,
|
|
1258
|
-
useExisting: forwardRef(() => UiDateInputComponent),
|
|
1259
|
-
multi: true,
|
|
1260
|
-
},
|
|
1261
|
-
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420'\"\n maxlength=\"10\"\n inputmode=\"numeric\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"displayValue\"\n (click)=\"openCalendar($event)\"\n (keydown)=\"allowDateKey($event)\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <lucide-icon\n [img]=\"calendarIcon\"\n class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"\n aria-hidden=\"true\"\n ></lucide-icon>\n\n @if (isCalendarOpen) {\n <div\n class=\"absolute left-0 top-full z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(-1, $event)\"\n >\n <lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showMonthPicker($event)\"\n >\n {{ monthName }}\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 transition hover:bg-slate-100\"\n (click)=\"showYearPicker($event)\"\n >\n {{ yearLabel }}\n </button>\n </div>\n @if (calendarView !== 'months') {\n <button\n type=\"button\"\n class=\"rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900\"\n aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C\"\n (click)=\"changeMonth(1, $event)\"\n >\n <lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon>\n </button>\n }\n </div>\n\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">\n @for (weekday of ['\u041F\u043D', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041F\u0442', '\u0421\u0431', '\u041D\u0434']; track weekday) {\n <span class=\"py-1\">{{ weekday }}</span>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-1\">\n @for (day of calendarDays; track day.date) {\n <button\n type=\"button\"\n class=\"aspect-square rounded-lg text-sm transition hover:bg-slate-100 hover:text-slate-900\"\n [class.text-slate-300]=\"!day.currentMonth\"\n [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\"\n [class.bg-slate-900]=\"isSelected(day.date)\"\n [class.text-white]=\"isSelected(day.date)\"\n (click)=\"selectDate(day.date, $event)\"\n >\n {{ day.day }}\n </button>\n }\n </div>\n } @else if (calendarView === 'months') {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (month of monthOptions; track month.value) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedMonth(month.value)\"\n [class.text-white]=\"isSelectedMonth(month.value)\"\n (click)=\"selectMonth(month.value, $event)\"\n >\n {{ month.label }}\n </button>\n }\n </div>\n } @else {\n <div class=\"grid grid-cols-3 gap-2\">\n @for (year of yearOptions; track year) {\n <button\n type=\"button\"\n class=\"rounded-lg px-2 py-3 text-sm text-slate-700 transition hover:bg-slate-100 hover:text-slate-900\"\n [class.bg-slate-900]=\"isSelectedYear(year)\"\n [class.text-white]=\"isSelectedYear(year)\"\n (click)=\"selectYear(year, $event)\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n" }]
|
|
1262
|
-
}], propDecorators: { label: [{
|
|
1263
|
-
type: Input
|
|
1264
|
-
}], hint: [{
|
|
1265
|
-
type: Input
|
|
1266
|
-
}], placeholder: [{
|
|
1267
|
-
type: Input
|
|
1268
|
-
}], name: [{
|
|
1269
|
-
type: Input
|
|
1270
|
-
}], id: [{
|
|
1271
|
-
type: Input
|
|
1272
|
-
}], rows: [{
|
|
1273
|
-
type: Input
|
|
1274
|
-
}], required: [{
|
|
1275
|
-
type: Input
|
|
1276
|
-
}], fullWidth: [{
|
|
1277
|
-
type: Input
|
|
1278
|
-
}], autocomplete: [{
|
|
1279
|
-
type: Input
|
|
1280
|
-
}], readonly: [{
|
|
1281
|
-
type: Input
|
|
1282
|
-
}], pattern: [{
|
|
1283
|
-
type: Input
|
|
1284
|
-
}], error: [{
|
|
1285
|
-
type: Input
|
|
1286
|
-
}], actions: [{
|
|
1287
|
-
type: Input
|
|
1288
|
-
}], actionTriggered: [{
|
|
1289
|
-
type: Output
|
|
1290
|
-
}], onDocumentClick: [{
|
|
1291
|
-
type: HostListener,
|
|
1292
|
-
args: ['document:click']
|
|
1293
|
-
}], onEscape: [{
|
|
1294
|
-
type: HostListener,
|
|
1295
|
-
args: ['document:keydown.escape']
|
|
1296
|
-
}] } });
|
|
1297
|
-
|
|
1298
|
-
class UiDatetimeInputComponent {
|
|
1299
|
-
cdr = inject(ChangeDetectorRef);
|
|
1300
|
-
popupService = inject(UiPopupService);
|
|
1301
|
-
closeActivePopup = () => this.closeCalendar();
|
|
1302
|
-
label;
|
|
1303
|
-
hint;
|
|
1304
|
-
placeholder;
|
|
1305
|
-
name;
|
|
1306
|
-
id;
|
|
1307
|
-
rows = 4;
|
|
1308
|
-
required = false;
|
|
1309
|
-
fullWidth = false;
|
|
1310
|
-
autocomplete = 'off';
|
|
1311
|
-
readonly = false;
|
|
1312
|
-
pattern;
|
|
1313
|
-
error;
|
|
1314
|
-
actions = [];
|
|
1315
|
-
type = 'datetime-local';
|
|
1316
|
-
calendarIcon = CalendarDays;
|
|
1317
|
-
previousMonthIcon = ChevronLeft;
|
|
1318
|
-
nextMonthIcon = ChevronRight;
|
|
1319
|
-
disabled = false;
|
|
1320
|
-
value = '';
|
|
1321
|
-
isCalendarOpen = false;
|
|
1322
|
-
calendarView = 'days';
|
|
1323
|
-
visibleMonth = this.startOfMonth(new Date());
|
|
1324
|
-
selectedHour = 12;
|
|
1325
|
-
selectedMinute = 0;
|
|
1326
|
-
static nextId = 0;
|
|
1327
|
-
generatedId = `ui-input-${UiDatetimeInputComponent.nextId++}`;
|
|
1328
|
-
actionTriggered = new EventEmitter();
|
|
1329
|
-
onChange = () => undefined;
|
|
1330
|
-
onTouched = () => undefined;
|
|
1331
|
-
get controlId() { return this.id ?? this.generatedId; }
|
|
1332
|
-
get selectedDate() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? new Date(+match[1], +match[2] - 1, +match[3]) : null; }
|
|
1333
|
-
get displayValue() { const match = String(this.value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); return match ? `${match[3]}.${match[2]}.${match[1]} ${match[4]}:${match[5]}` : String(this.value ?? ''); }
|
|
1334
|
-
get monthName() { return this.visibleMonth.toLocaleDateString('uk-UA', { month: 'long' }); }
|
|
1335
|
-
get yearLabel() { return this.visibleMonth.getFullYear(); }
|
|
1336
|
-
get monthOptions() { return Array.from({ length: 12 }, (_, value) => ({ value, label: new Date(2000, value, 1).toLocaleDateString('uk-UA', { month: 'long' }) })); }
|
|
1337
|
-
get yearOptions() { const year = this.visibleMonth.getFullYear(); return Array.from({ length: 12 }, (_, index) => year - 5 + index); }
|
|
1338
|
-
get calendarDays() {
|
|
1339
|
-
const first = this.startOfMonth(this.visibleMonth);
|
|
1340
|
-
const offset = (first.getDay() + 6) % 7;
|
|
1341
|
-
const start = new Date(first);
|
|
1342
|
-
start.setDate(start.getDate() - offset);
|
|
1343
|
-
return Array.from({ length: 42 }, (_, index) => { const date = new Date(start); date.setDate(start.getDate() + index); return { date: this.toDateValue(date), day: date.getDate(), currentMonth: date.getMonth() === this.visibleMonth.getMonth() }; });
|
|
1344
|
-
}
|
|
1345
|
-
writeValue(value) { this.value = value ?? ''; const date = this.selectedDate; if (date) {
|
|
1346
|
-
this.visibleMonth = this.startOfMonth(date);
|
|
1347
|
-
this.selectedHour = +(String(value).slice(11, 13));
|
|
1348
|
-
this.selectedMinute = +(String(value).slice(14, 16));
|
|
1349
|
-
} this.cdr.markForCheck(); }
|
|
1350
|
-
registerOnChange(fn) { this.onChange = fn; }
|
|
1351
|
-
registerOnTouched(fn) { this.onTouched = fn; }
|
|
1352
|
-
setDisabledState(disabled) { this.disabled = disabled; this.cdr.markForCheck(); }
|
|
1353
|
-
openCalendar(event) { event.stopPropagation(); if (this.disabled)
|
|
1354
|
-
return; this.isCalendarOpen = true; this.calendarView = 'days'; this.popupService.open(this.closeActivePopup); this.cdr.markForCheck(); }
|
|
1355
|
-
closeCalendar() { this.isCalendarOpen = false; this.popupService.close(this.closeActivePopup); this.cdr.markForCheck(); }
|
|
1356
|
-
changeMonth(offset, event) { event.stopPropagation(); const year = this.visibleMonth.getFullYear(); const month = this.visibleMonth.getMonth(); this.visibleMonth = this.calendarView === 'years' ? new Date(year + offset * 12, month, 1) : new Date(year + (this.calendarView === 'months' ? offset : 0), month + (this.calendarView === 'days' ? offset : 0), 1); this.cdr.markForCheck(); }
|
|
1357
|
-
showMonthPicker(event) { event.stopPropagation(); this.calendarView = 'months'; this.cdr.markForCheck(); }
|
|
1358
|
-
showYearPicker(event) { event.stopPropagation(); this.calendarView = 'years'; this.cdr.markForCheck(); }
|
|
1359
|
-
selectMonth(month, event) { event.stopPropagation(); this.visibleMonth = new Date(this.visibleMonth.getFullYear(), month, 1); this.calendarView = 'days'; this.cdr.markForCheck(); }
|
|
1360
|
-
selectYear(year, event) { event.stopPropagation(); this.visibleMonth = new Date(year, this.visibleMonth.getMonth(), 1); this.calendarView = 'months'; this.cdr.markForCheck(); }
|
|
1361
|
-
selectDate(dateValue, event) { event.stopPropagation(); const date = this.parseDate(dateValue); this.visibleMonth = this.startOfMonth(date); this.setDateTime(dateValue); }
|
|
1362
|
-
setDateTime(dateValue, close = true) { const hour = String(this.selectedHour).padStart(2, '0'); const minute = String(this.selectedMinute).padStart(2, '0'); this.value = `${dateValue}T${hour}:${minute}`; this.onChange(this.value); if (close)
|
|
1363
|
-
this.closeCalendar(); if (close)
|
|
1364
|
-
this.onTouched(); this.cdr.markForCheck(); }
|
|
1365
|
-
onTimeChange() { const date = this.selectedDate; if (date) {
|
|
1366
|
-
this.setDateTime(this.toDateValue(date), false);
|
|
1367
|
-
} }
|
|
1368
|
-
onValueChange(input) { const masked = this.applyMask(input); const match = masked.match(/^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/); if (match) {
|
|
1369
|
-
const date = new Date(+match[3], +match[2] - 1, +match[1]);
|
|
1370
|
-
if (date.getFullYear() === +match[3] && date.getMonth() === +match[2] - 1 && date.getDate() === +match[1] && +match[4] < 24 && +match[5] < 60) {
|
|
1371
|
-
this.value = `${match[3]}-${match[2]}-${match[1]}T${match[4]}:${match[5]}`;
|
|
1372
|
-
this.visibleMonth = this.startOfMonth(date);
|
|
1373
|
-
this.selectedHour = +match[4];
|
|
1374
|
-
this.selectedMinute = +match[5];
|
|
1375
|
-
}
|
|
1376
|
-
else
|
|
1377
|
-
this.value = masked;
|
|
1101
|
+
else
|
|
1102
|
+
this.value = masked;
|
|
1378
1103
|
}
|
|
1379
1104
|
else
|
|
1380
1105
|
this.value = masked; this.onChange(this.value); this.cdr.markForCheck(); }
|
|
@@ -1393,12 +1118,18 @@ class UiDatetimeInputComponent {
|
|
|
1393
1118
|
startOfMonth(date) { return new Date(date.getFullYear(), date.getMonth(), 1); }
|
|
1394
1119
|
parseDate(value) { const [year, month, day] = value.split('-').map(Number); return year && month && day ? new Date(year, month - 1, day) : null; }
|
|
1395
1120
|
toDateValue(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; }
|
|
1396
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1397
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1121
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDatetimeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1122
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputDatetimeComponent, isStandalone: true, selector: "input-datetime", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
|
|
1123
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
|
|
1124
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
|
|
1125
|
+
], ngImport: i0, template: "<div class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\" [class.w-full]=\"fullWidth\">\n @if (label) { <span class=\"text-[0.7rem] font-semibold text-slate-600\">{{ label }} @if (required) { <span class=\"text-rose-500\"> *</span> }</span> }\n <div class=\"flex items-end gap-2\"><div class=\"min-w-0 flex-1\"><div class=\"relative\">\n <input type=\"text\" [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420 \u0413\u0413:\u0425\u0425'\" maxlength=\"16\" inputmode=\"numeric\" [attr.name]=\"name\" [attr.autocomplete]=\"autocomplete\" [disabled]=\"disabled\" class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" [attr.id]=\"controlId\" [value]=\"displayValue\" (click)=\"openCalendar($event)\" (keydown)=\"allowDateKey($event)\" (input)=\"onValueChange($any($event.target).value)\" (blur)=\"onTouched()\" />\n <lucide-icon [img]=\"calendarIcon\" class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"></lucide-icon>\n @if (isCalendarOpen) { <div class=\"absolute left-0 top-full z-50 mt-2 w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\" (click)=\"$event.stopPropagation()\">\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(-1, $event)\"><lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n <div class=\"flex items-center gap-1\"><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 hover:bg-slate-100\" (click)=\"showMonthPicker($event)\">{{ monthName }}</button><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 hover:bg-slate-100\" (click)=\"showYearPicker($event)\">{{ yearLabel }}</button></div>\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(1, $event)\"><lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n </div>\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">@for (weekday of ['\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431','\u041D\u0434']; track weekday) { <span class=\"py-1\">{{ weekday }}</span> }</div>\n <div class=\"grid grid-cols-7 gap-1\">@for (day of calendarDays; track day.date) { <button type=\"button\" class=\"aspect-square rounded-lg text-sm hover:bg-slate-100 hover:text-slate-900\" [class.text-slate-300]=\"!day.currentMonth\" [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\" [class.bg-slate-900]=\"isSelected(day.date)\" [class.text-white]=\"isSelected(day.date)\" (click)=\"selectDate(day.date, $event)\">{{ day.day }}</button> }</div>\n <div class=\"mt-4 grid grid-cols-2 gap-2 border-t border-slate-100 pt-3\"><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0413\u043E\u0434\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"23\" [value]=\"selectedHour\" (input)=\"selectedHour = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0425\u0432\u0438\u043B\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"59\" [value]=\"selectedMinute\" (input)=\"selectedMinute = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label></div>\n } @else if (calendarView === 'months') { <div class=\"grid grid-cols-3 gap-2\">@for (month of monthOptions; track month.value) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedMonth(month.value)\" [class.text-white]=\"isSelectedMonth(month.value)\" (click)=\"selectMonth(month.value, $event)\">{{ month.label }}</button> }</div>\n } @else { <div class=\"grid grid-cols-3 gap-2\">@for (year of yearOptions; track year) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedYear(year)\" [class.text-white]=\"isSelectedYear(year)\" (click)=\"selectYear(year, $event)\">{{ year }}</button> }</div> }\n </div> }\n </div></div>\n @if (actions.length) { <div class=\"flex flex-none items-center gap-2 pb-0.5\">@for (action of actions; track action.id) { <ui-button type=\"button\" [variant]=\"action.variant ?? 'secondary'\" [icon]=\"action.icon || ''\" size=\"sm\" [disabled]=\"disabled\" (click)=\"triggerAction(action)\">{{ action.label }}</ui-button> }</div> }\n </div>\n @if (hint) { <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p> } @if (error) { <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p> }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1398
1126
|
}
|
|
1399
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1127
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDatetimeComponent, decorators: [{
|
|
1400
1128
|
type: Component,
|
|
1401
|
-
args: [{ selector: '
|
|
1129
|
+
args: [{ selector: 'input-datetime', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1130
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
|
|
1131
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDatetimeComponent), multi: true },
|
|
1132
|
+
], template: "<div class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\" [class.w-full]=\"fullWidth\">\n @if (label) { <span class=\"text-[0.7rem] font-semibold text-slate-600\">{{ label }} @if (required) { <span class=\"text-rose-500\"> *</span> }</span> }\n <div class=\"flex items-end gap-2\"><div class=\"min-w-0 flex-1\"><div class=\"relative\">\n <input type=\"text\" [attr.placeholder]=\"placeholder ?? '\u0414\u0414.\u041C\u041C.\u0420\u0420\u0420\u0420 \u0413\u0413:\u0425\u0425'\" maxlength=\"16\" inputmode=\"numeric\" [attr.name]=\"name\" [attr.autocomplete]=\"autocomplete\" [disabled]=\"disabled\" class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" [attr.id]=\"controlId\" [value]=\"displayValue\" (click)=\"openCalendar($event)\" (keydown)=\"allowDateKey($event)\" (input)=\"onValueChange($any($event.target).value)\" (blur)=\"onTouched()\" />\n <lucide-icon [img]=\"calendarIcon\" class=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400\"></lucide-icon>\n @if (isCalendarOpen) { <div class=\"absolute left-0 top-full z-50 mt-2 w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-4 shadow-xl\" (click)=\"$event.stopPropagation()\">\n <div class=\"mb-4 flex items-center justify-between\" [class.justify-center]=\"calendarView === 'months'\">\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(-1, $event)\"><lucide-icon [img]=\"previousMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n <div class=\"flex items-center gap-1\"><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold capitalize text-slate-900 hover:bg-slate-100\" (click)=\"showMonthPicker($event)\">{{ monthName }}</button><button type=\"button\" class=\"rounded-lg px-2 py-1 text-sm font-semibold text-slate-900 hover:bg-slate-100\" (click)=\"showYearPicker($event)\">{{ yearLabel }}</button></div>\n @if (calendarView !== 'months') { <button type=\"button\" class=\"rounded-lg p-2 text-slate-500 hover:bg-slate-100\" aria-label=\"\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043F\u0435\u0440\u0456\u043E\u0434\" (click)=\"changeMonth(1, $event)\"><lucide-icon [img]=\"nextMonthIcon\" class=\"h-4 w-4\"></lucide-icon></button> }\n </div>\n @if (calendarView === 'days') {\n <div class=\"mb-2 grid grid-cols-7 text-center text-xs font-medium text-slate-400\">@for (weekday of ['\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431','\u041D\u0434']; track weekday) { <span class=\"py-1\">{{ weekday }}</span> }</div>\n <div class=\"grid grid-cols-7 gap-1\">@for (day of calendarDays; track day.date) { <button type=\"button\" class=\"aspect-square rounded-lg text-sm hover:bg-slate-100 hover:text-slate-900\" [class.text-slate-300]=\"!day.currentMonth\" [class.text-slate-700]=\"day.currentMonth && !isSelected(day.date)\" [class.bg-slate-900]=\"isSelected(day.date)\" [class.text-white]=\"isSelected(day.date)\" (click)=\"selectDate(day.date, $event)\">{{ day.day }}</button> }</div>\n <div class=\"mt-4 grid grid-cols-2 gap-2 border-t border-slate-100 pt-3\"><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0413\u043E\u0434\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"23\" [value]=\"selectedHour\" (input)=\"selectedHour = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label><label class=\"text-[0.65rem] font-medium text-slate-500\">\u0425\u0432\u0438\u043B\u0438\u043D\u0438<input type=\"number\" min=\"0\" max=\"59\" [value]=\"selectedMinute\" (input)=\"selectedMinute = +$any($event.target).value; onTimeChange()\" class=\"mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\" /></label></div>\n } @else if (calendarView === 'months') { <div class=\"grid grid-cols-3 gap-2\">@for (month of monthOptions; track month.value) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm capitalize text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedMonth(month.value)\" [class.text-white]=\"isSelectedMonth(month.value)\" (click)=\"selectMonth(month.value, $event)\">{{ month.label }}</button> }</div>\n } @else { <div class=\"grid grid-cols-3 gap-2\">@for (year of yearOptions; track year) { <button type=\"button\" class=\"rounded-lg px-2 py-3 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900\" [class.bg-slate-900]=\"isSelectedYear(year)\" [class.text-white]=\"isSelectedYear(year)\" (click)=\"selectYear(year, $event)\">{{ year }}</button> }</div> }\n </div> }\n </div></div>\n @if (actions.length) { <div class=\"flex flex-none items-center gap-2 pb-0.5\">@for (action of actions; track action.id) { <ui-button type=\"button\" [variant]=\"action.variant ?? 'secondary'\" [icon]=\"action.icon || ''\" size=\"sm\" [disabled]=\"disabled\" (click)=\"triggerAction(action)\">{{ action.label }}</ui-button> }</div> }\n </div>\n @if (hint) { <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p> } @if (error) { <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p> }\n</div>\n" }]
|
|
1402
1133
|
}], propDecorators: { label: [{
|
|
1403
1134
|
type: Input
|
|
1404
1135
|
}], hint: [{
|
|
@@ -1417,8 +1148,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1417
1148
|
type: Input
|
|
1418
1149
|
}], autocomplete: [{
|
|
1419
1150
|
type: Input
|
|
1420
|
-
}], readonly: [{
|
|
1421
|
-
type: Input
|
|
1422
1151
|
}], pattern: [{
|
|
1423
1152
|
type: Input
|
|
1424
1153
|
}], error: [{
|
|
@@ -1435,7 +1164,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1435
1164
|
args: ['document:keydown.escape']
|
|
1436
1165
|
}] } });
|
|
1437
1166
|
|
|
1438
|
-
class
|
|
1167
|
+
class InputEmailComponent {
|
|
1439
1168
|
cdr = inject(ChangeDetectorRef);
|
|
1440
1169
|
label;
|
|
1441
1170
|
hint;
|
|
@@ -1448,14 +1177,13 @@ class UiEmailInputComponent {
|
|
|
1448
1177
|
required = false;
|
|
1449
1178
|
fullWidth = false;
|
|
1450
1179
|
autocomplete;
|
|
1451
|
-
readonly = false;
|
|
1452
1180
|
pattern;
|
|
1453
1181
|
error;
|
|
1454
1182
|
actions = [];
|
|
1455
1183
|
disabled = false;
|
|
1456
1184
|
value = '';
|
|
1457
1185
|
static nextId = 0;
|
|
1458
|
-
generatedId = `
|
|
1186
|
+
generatedId = `input-email-${InputEmailComponent.nextId++}`;
|
|
1459
1187
|
actionTriggered = new EventEmitter();
|
|
1460
1188
|
get controlId() {
|
|
1461
1189
|
return this.id ?? this.generatedId;
|
|
@@ -1472,6 +1200,7 @@ class UiEmailInputComponent {
|
|
|
1472
1200
|
registerOnTouched(fn) {
|
|
1473
1201
|
this.onTouched = fn;
|
|
1474
1202
|
}
|
|
1203
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern, true); }
|
|
1475
1204
|
setDisabledState(isDisabled) {
|
|
1476
1205
|
this.disabled = isDisabled;
|
|
1477
1206
|
this.cdr.markForCheck();
|
|
@@ -1484,24 +1213,26 @@ class UiEmailInputComponent {
|
|
|
1484
1213
|
triggerAction(action) {
|
|
1485
1214
|
this.actionTriggered.emit(action);
|
|
1486
1215
|
}
|
|
1487
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1488
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1216
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputEmailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1217
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputEmailComponent, isStandalone: true, selector: "input-email", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
1489
1218
|
{
|
|
1490
1219
|
provide: NG_VALUE_ACCESSOR,
|
|
1491
|
-
useExisting: forwardRef(() =>
|
|
1220
|
+
useExisting: forwardRef(() => InputEmailComponent),
|
|
1492
1221
|
multi: true,
|
|
1493
1222
|
},
|
|
1494
|
-
|
|
1223
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputEmailComponent), multi: true },
|
|
1224
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1495
1225
|
}
|
|
1496
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1226
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputEmailComponent, decorators: [{
|
|
1497
1227
|
type: Component,
|
|
1498
|
-
args: [{ selector: '
|
|
1228
|
+
args: [{ selector: 'input-email', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1499
1229
|
{
|
|
1500
1230
|
provide: NG_VALUE_ACCESSOR,
|
|
1501
|
-
useExisting: forwardRef(() =>
|
|
1231
|
+
useExisting: forwardRef(() => InputEmailComponent),
|
|
1502
1232
|
multi: true,
|
|
1503
1233
|
},
|
|
1504
|
-
|
|
1234
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputEmailComponent), multi: true },
|
|
1235
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
|
|
1505
1236
|
}], propDecorators: { label: [{
|
|
1506
1237
|
type: Input
|
|
1507
1238
|
}], hint: [{
|
|
@@ -1520,8 +1251,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1520
1251
|
type: Input
|
|
1521
1252
|
}], autocomplete: [{
|
|
1522
1253
|
type: Input
|
|
1523
|
-
}], readonly: [{
|
|
1524
|
-
type: Input
|
|
1525
1254
|
}], pattern: [{
|
|
1526
1255
|
type: Input
|
|
1527
1256
|
}], error: [{
|
|
@@ -1532,7 +1261,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1532
1261
|
type: Output
|
|
1533
1262
|
}] } });
|
|
1534
1263
|
|
|
1535
|
-
class
|
|
1264
|
+
class InputNumberComponent {
|
|
1536
1265
|
cdr = inject(ChangeDetectorRef);
|
|
1537
1266
|
label;
|
|
1538
1267
|
hint;
|
|
@@ -1545,14 +1274,13 @@ class UiNumberInputComponent {
|
|
|
1545
1274
|
required = false;
|
|
1546
1275
|
fullWidth = false;
|
|
1547
1276
|
autocomplete;
|
|
1548
|
-
readonly = false;
|
|
1549
1277
|
disabled = false;
|
|
1550
1278
|
pattern;
|
|
1551
1279
|
error;
|
|
1552
1280
|
actions = [];
|
|
1553
1281
|
value = '';
|
|
1554
1282
|
static nextId = 0;
|
|
1555
|
-
generatedId = `
|
|
1283
|
+
generatedId = `input-number-${InputNumberComponent.nextId++}`;
|
|
1556
1284
|
actionTriggered = new EventEmitter();
|
|
1557
1285
|
get controlId() {
|
|
1558
1286
|
return this.id ?? this.generatedId;
|
|
@@ -1569,6 +1297,7 @@ class UiNumberInputComponent {
|
|
|
1569
1297
|
registerOnTouched(fn) {
|
|
1570
1298
|
this.onTouched = fn;
|
|
1571
1299
|
}
|
|
1300
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern); }
|
|
1572
1301
|
setDisabledState(isDisabled) {
|
|
1573
1302
|
this.disabled = isDisabled;
|
|
1574
1303
|
this.cdr.markForCheck();
|
|
@@ -1584,24 +1313,26 @@ class UiNumberInputComponent {
|
|
|
1584
1313
|
triggerAction(action) {
|
|
1585
1314
|
this.actionTriggered.emit(action);
|
|
1586
1315
|
}
|
|
1587
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1588
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1316
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputNumberComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1317
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputNumberComponent, isStandalone: true, selector: "input-number", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", disabled: "disabled", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
1589
1318
|
{
|
|
1590
1319
|
provide: NG_VALUE_ACCESSOR,
|
|
1591
|
-
useExisting: forwardRef(() =>
|
|
1320
|
+
useExisting: forwardRef(() => InputNumberComponent),
|
|
1592
1321
|
multi: true,
|
|
1593
1322
|
},
|
|
1594
|
-
|
|
1323
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputNumberComponent), multi: true },
|
|
1324
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full appearance-none rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (wheel)=\"preventWheelChange($event)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", styles: ["input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none;-webkit-appearance:none;margin:0}\n", "input[type=number]{-moz-appearance:textfield}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1595
1325
|
}
|
|
1596
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputNumberComponent, decorators: [{
|
|
1597
1327
|
type: Component,
|
|
1598
|
-
args: [{ selector: '
|
|
1328
|
+
args: [{ selector: 'input-number', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1599
1329
|
{
|
|
1600
1330
|
provide: NG_VALUE_ACCESSOR,
|
|
1601
|
-
useExisting: forwardRef(() =>
|
|
1331
|
+
useExisting: forwardRef(() => InputNumberComponent),
|
|
1602
1332
|
multi: true,
|
|
1603
1333
|
},
|
|
1604
|
-
|
|
1334
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputNumberComponent), multi: true },
|
|
1335
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full appearance-none rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (wheel)=\"preventWheelChange($event)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", styles: ["input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{appearance:none;-webkit-appearance:none;margin:0}\n", "input[type=number]{-moz-appearance:textfield}\n"] }]
|
|
1605
1336
|
}], propDecorators: { label: [{
|
|
1606
1337
|
type: Input
|
|
1607
1338
|
}], hint: [{
|
|
@@ -1620,8 +1351,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1620
1351
|
type: Input
|
|
1621
1352
|
}], autocomplete: [{
|
|
1622
1353
|
type: Input
|
|
1623
|
-
}], readonly: [{
|
|
1624
|
-
type: Input
|
|
1625
1354
|
}], disabled: [{
|
|
1626
1355
|
type: Input
|
|
1627
1356
|
}], pattern: [{
|
|
@@ -1634,7 +1363,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1634
1363
|
type: Output
|
|
1635
1364
|
}] } });
|
|
1636
1365
|
|
|
1637
|
-
class
|
|
1366
|
+
class InputPasswordComponent {
|
|
1638
1367
|
cdr = inject(ChangeDetectorRef);
|
|
1639
1368
|
label;
|
|
1640
1369
|
hint;
|
|
@@ -1649,7 +1378,6 @@ class UiPasswordInputComponent {
|
|
|
1649
1378
|
required = false;
|
|
1650
1379
|
fullWidth = false;
|
|
1651
1380
|
autocomplete;
|
|
1652
|
-
readonly = false;
|
|
1653
1381
|
pattern;
|
|
1654
1382
|
error;
|
|
1655
1383
|
actions = [];
|
|
@@ -1657,7 +1385,7 @@ class UiPasswordInputComponent {
|
|
|
1657
1385
|
isPasswordVisible = false;
|
|
1658
1386
|
value = '';
|
|
1659
1387
|
static nextId = 0;
|
|
1660
|
-
generatedId = `
|
|
1388
|
+
generatedId = `input-password-${InputPasswordComponent.nextId++}`;
|
|
1661
1389
|
actionTriggered = new EventEmitter();
|
|
1662
1390
|
get controlId() {
|
|
1663
1391
|
return this.id ?? this.generatedId;
|
|
@@ -1674,6 +1402,7 @@ class UiPasswordInputComponent {
|
|
|
1674
1402
|
registerOnTouched(fn) {
|
|
1675
1403
|
this.onTouched = fn;
|
|
1676
1404
|
}
|
|
1405
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern); }
|
|
1677
1406
|
setDisabledState(isDisabled) {
|
|
1678
1407
|
this.disabled = isDisabled;
|
|
1679
1408
|
this.cdr.markForCheck();
|
|
@@ -1691,24 +1420,26 @@ class UiPasswordInputComponent {
|
|
|
1691
1420
|
triggerAction(action) {
|
|
1692
1421
|
this.actionTriggered.emit(action);
|
|
1693
1422
|
}
|
|
1694
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1695
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1423
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPasswordComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1424
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputPasswordComponent, isStandalone: true, selector: "input-password", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
1696
1425
|
{
|
|
1697
1426
|
provide: NG_VALUE_ACCESSOR,
|
|
1698
|
-
useExisting: forwardRef(() =>
|
|
1427
|
+
useExisting: forwardRef(() => InputPasswordComponent),
|
|
1699
1428
|
multi: true,
|
|
1700
1429
|
},
|
|
1701
|
-
|
|
1430
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputPasswordComponent), multi: true },
|
|
1431
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n [attr.type]=\"isPasswordVisible ? 'text' : type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <button\n type=\"button\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.aria-label]=\"isPasswordVisible ? '\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C' : '\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C'\"\n [disabled]=\"disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"togglePassword($event)\"\n >\n <lucide-icon\n [img]=\"isPasswordVisible ? hidePasswordIcon : showPasswordIcon\"\n class=\"h-4 w-4\"\n aria-hidden=\"true\"\n ></lucide-icon>\n </button>\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1702
1432
|
}
|
|
1703
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1433
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPasswordComponent, decorators: [{
|
|
1704
1434
|
type: Component,
|
|
1705
|
-
args: [{ selector: '
|
|
1435
|
+
args: [{ selector: 'input-password', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1706
1436
|
{
|
|
1707
1437
|
provide: NG_VALUE_ACCESSOR,
|
|
1708
|
-
useExisting: forwardRef(() =>
|
|
1438
|
+
useExisting: forwardRef(() => InputPasswordComponent),
|
|
1709
1439
|
multi: true,
|
|
1710
1440
|
},
|
|
1711
|
-
|
|
1441
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputPasswordComponent), multi: true },
|
|
1442
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <div class=\"relative\">\n <input\n [attr.type]=\"isPasswordVisible ? 'text' : type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n <button\n type=\"button\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.aria-label]=\"isPasswordVisible ? '\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C' : '\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u043F\u0430\u0440\u043E\u043B\u044C'\"\n [disabled]=\"disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"togglePassword($event)\"\n >\n <lucide-icon\n [img]=\"isPasswordVisible ? hidePasswordIcon : showPasswordIcon\"\n class=\"h-4 w-4\"\n aria-hidden=\"true\"\n ></lucide-icon>\n </button>\n </div>\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n" }]
|
|
1712
1443
|
}], propDecorators: { label: [{
|
|
1713
1444
|
type: Input
|
|
1714
1445
|
}], hint: [{
|
|
@@ -1727,8 +1458,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1727
1458
|
type: Input
|
|
1728
1459
|
}], autocomplete: [{
|
|
1729
1460
|
type: Input
|
|
1730
|
-
}], readonly: [{
|
|
1731
|
-
type: Input
|
|
1732
1461
|
}], pattern: [{
|
|
1733
1462
|
type: Input
|
|
1734
1463
|
}], error: [{
|
|
@@ -1739,7 +1468,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1739
1468
|
type: Output
|
|
1740
1469
|
}] } });
|
|
1741
1470
|
|
|
1742
|
-
class
|
|
1471
|
+
class InputTextComponent {
|
|
1743
1472
|
cdr = inject(ChangeDetectorRef);
|
|
1744
1473
|
label;
|
|
1745
1474
|
hint;
|
|
@@ -1752,14 +1481,13 @@ class UiTextInputComponent {
|
|
|
1752
1481
|
required = false;
|
|
1753
1482
|
fullWidth = false;
|
|
1754
1483
|
autocomplete;
|
|
1755
|
-
readonly = false;
|
|
1756
1484
|
pattern;
|
|
1757
1485
|
error;
|
|
1758
1486
|
actions = [];
|
|
1759
1487
|
disabled = false;
|
|
1760
1488
|
value = '';
|
|
1761
1489
|
static nextId = 0;
|
|
1762
|
-
generatedId = `
|
|
1490
|
+
generatedId = `input-text-${InputTextComponent.nextId++}`;
|
|
1763
1491
|
actionTriggered = new EventEmitter();
|
|
1764
1492
|
get controlId() {
|
|
1765
1493
|
return this.id ?? this.generatedId;
|
|
@@ -1776,6 +1504,7 @@ class UiTextInputComponent {
|
|
|
1776
1504
|
registerOnTouched(fn) {
|
|
1777
1505
|
this.onTouched = fn;
|
|
1778
1506
|
}
|
|
1507
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern); }
|
|
1779
1508
|
setDisabledState(isDisabled) {
|
|
1780
1509
|
this.disabled = isDisabled;
|
|
1781
1510
|
this.cdr.markForCheck();
|
|
@@ -1788,24 +1517,26 @@ class UiTextInputComponent {
|
|
|
1788
1517
|
triggerAction(action) {
|
|
1789
1518
|
this.actionTriggered.emit(action);
|
|
1790
1519
|
}
|
|
1791
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1792
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1520
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1521
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputTextComponent, isStandalone: true, selector: "input-text", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
1793
1522
|
{
|
|
1794
1523
|
provide: NG_VALUE_ACCESSOR,
|
|
1795
|
-
useExisting: forwardRef(() =>
|
|
1524
|
+
useExisting: forwardRef(() => InputTextComponent),
|
|
1796
1525
|
multi: true,
|
|
1797
1526
|
},
|
|
1798
|
-
|
|
1527
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextComponent), multi: true },
|
|
1528
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1799
1529
|
}
|
|
1800
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1530
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextComponent, decorators: [{
|
|
1801
1531
|
type: Component,
|
|
1802
|
-
args: [{ selector: '
|
|
1532
|
+
args: [{ selector: 'input-text', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1803
1533
|
{
|
|
1804
1534
|
provide: NG_VALUE_ACCESSOR,
|
|
1805
|
-
useExisting: forwardRef(() =>
|
|
1535
|
+
useExisting: forwardRef(() => InputTextComponent),
|
|
1806
1536
|
multi: true,
|
|
1807
1537
|
},
|
|
1808
|
-
|
|
1538
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextComponent), multi: true },
|
|
1539
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
|
|
1809
1540
|
}], propDecorators: { label: [{
|
|
1810
1541
|
type: Input
|
|
1811
1542
|
}], hint: [{
|
|
@@ -1824,8 +1555,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1824
1555
|
type: Input
|
|
1825
1556
|
}], autocomplete: [{
|
|
1826
1557
|
type: Input
|
|
1827
|
-
}], readonly: [{
|
|
1828
|
-
type: Input
|
|
1829
1558
|
}], pattern: [{
|
|
1830
1559
|
type: Input
|
|
1831
1560
|
}], error: [{
|
|
@@ -1836,7 +1565,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1836
1565
|
type: Output
|
|
1837
1566
|
}] } });
|
|
1838
1567
|
|
|
1839
|
-
class
|
|
1568
|
+
class InputTextareaComponent {
|
|
1840
1569
|
cdr = inject(ChangeDetectorRef);
|
|
1841
1570
|
label;
|
|
1842
1571
|
hint;
|
|
@@ -1849,14 +1578,13 @@ class UiTextareaInputComponent {
|
|
|
1849
1578
|
required = false;
|
|
1850
1579
|
fullWidth = false;
|
|
1851
1580
|
autocomplete;
|
|
1852
|
-
readonly = false;
|
|
1853
1581
|
pattern;
|
|
1854
1582
|
error;
|
|
1855
1583
|
actions = [];
|
|
1856
1584
|
disabled = false;
|
|
1857
1585
|
value = '';
|
|
1858
1586
|
static nextId = 0;
|
|
1859
|
-
generatedId = `
|
|
1587
|
+
generatedId = `input-textarea-${InputTextareaComponent.nextId++}`;
|
|
1860
1588
|
actionTriggered = new EventEmitter();
|
|
1861
1589
|
get controlId() {
|
|
1862
1590
|
return this.id ?? this.generatedId;
|
|
@@ -1873,6 +1601,7 @@ class UiTextareaInputComponent {
|
|
|
1873
1601
|
registerOnTouched(fn) {
|
|
1874
1602
|
this.onTouched = fn;
|
|
1875
1603
|
}
|
|
1604
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern); }
|
|
1876
1605
|
setDisabledState(isDisabled) {
|
|
1877
1606
|
this.disabled = isDisabled;
|
|
1878
1607
|
this.cdr.markForCheck();
|
|
@@ -1885,24 +1614,26 @@ class UiTextareaInputComponent {
|
|
|
1885
1614
|
triggerAction(action) {
|
|
1886
1615
|
this.actionTriggered.emit(action);
|
|
1887
1616
|
}
|
|
1888
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1889
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1617
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1618
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputTextareaComponent, isStandalone: true, selector: "input-textarea", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
1890
1619
|
{
|
|
1891
1620
|
provide: NG_VALUE_ACCESSOR,
|
|
1892
|
-
useExisting: forwardRef(() =>
|
|
1621
|
+
useExisting: forwardRef(() => InputTextareaComponent),
|
|
1893
1622
|
multi: true,
|
|
1894
1623
|
},
|
|
1895
|
-
|
|
1624
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextareaComponent), multi: true },
|
|
1625
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1896
1626
|
}
|
|
1897
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1627
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputTextareaComponent, decorators: [{
|
|
1898
1628
|
type: Component,
|
|
1899
|
-
args: [{ selector: '
|
|
1629
|
+
args: [{ selector: 'input-textarea', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1900
1630
|
{
|
|
1901
1631
|
provide: NG_VALUE_ACCESSOR,
|
|
1902
|
-
useExisting: forwardRef(() =>
|
|
1632
|
+
useExisting: forwardRef(() => InputTextareaComponent),
|
|
1903
1633
|
multi: true,
|
|
1904
1634
|
},
|
|
1905
|
-
|
|
1635
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextareaComponent), multi: true },
|
|
1636
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
|
|
1906
1637
|
}], propDecorators: { label: [{
|
|
1907
1638
|
type: Input
|
|
1908
1639
|
}], hint: [{
|
|
@@ -1921,8 +1652,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1921
1652
|
type: Input
|
|
1922
1653
|
}], autocomplete: [{
|
|
1923
1654
|
type: Input
|
|
1924
|
-
}], readonly: [{
|
|
1925
|
-
type: Input
|
|
1926
1655
|
}], pattern: [{
|
|
1927
1656
|
type: Input
|
|
1928
1657
|
}], error: [{
|
|
@@ -1933,96 +1662,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1933
1662
|
type: Output
|
|
1934
1663
|
}] } });
|
|
1935
1664
|
|
|
1936
|
-
|
|
1937
|
-
text: UiTextInputComponent,
|
|
1938
|
-
number: UiNumberInputComponent,
|
|
1939
|
-
date: UiDateInputComponent,
|
|
1940
|
-
datetime: UiDatetimeInputComponent,
|
|
1941
|
-
email: UiEmailInputComponent,
|
|
1942
|
-
password: UiPasswordInputComponent,
|
|
1943
|
-
textarea: UiTextareaInputComponent,
|
|
1944
|
-
};
|
|
1945
|
-
|
|
1946
|
-
class UiFormInputHostComponent {
|
|
1947
|
-
field;
|
|
1948
|
-
value;
|
|
1949
|
-
error = '';
|
|
1950
|
-
disabled = false;
|
|
1951
|
-
actions = [];
|
|
1952
|
-
options = [];
|
|
1953
|
-
valueChange = new EventEmitter();
|
|
1954
|
-
actionTriggered = new EventEmitter();
|
|
1955
|
-
viewContainer = inject(ViewContainerRef);
|
|
1956
|
-
componentRef;
|
|
1957
|
-
ngOnChanges(changes) {
|
|
1958
|
-
if (changes['field']) {
|
|
1959
|
-
this.render();
|
|
1960
|
-
return;
|
|
1961
|
-
}
|
|
1962
|
-
this.syncInputs();
|
|
1963
|
-
}
|
|
1964
|
-
render() {
|
|
1965
|
-
this.viewContainer.clear();
|
|
1966
|
-
const inputType = String(this.field?.type || 'text');
|
|
1967
|
-
const componentType = UI_FORM_INPUT_REGISTRY[inputType] || UI_FORM_INPUT_REGISTRY.text;
|
|
1968
|
-
this.componentRef = this.viewContainer.createComponent(componentType);
|
|
1969
|
-
this.syncInputs();
|
|
1970
|
-
const instance = this.componentRef.instance;
|
|
1971
|
-
instance.registerOnChange?.((nextValue) => this.valueChange.emit(nextValue));
|
|
1972
|
-
instance.registerOnTouched?.(() => undefined);
|
|
1973
|
-
instance.actionTriggered?.subscribe((action) => this.actionTriggered.emit(action));
|
|
1974
|
-
}
|
|
1975
|
-
syncInputs() {
|
|
1976
|
-
if (!this.componentRef || !this.field)
|
|
1977
|
-
return;
|
|
1978
|
-
const field = this.field;
|
|
1979
|
-
const setInput = (name, value) => this.componentRef?.setInput(name, value);
|
|
1980
|
-
setInput('name', field.id);
|
|
1981
|
-
setInput('id', field.id);
|
|
1982
|
-
setInput('label', field.label);
|
|
1983
|
-
setInput('placeholder', field.placeholder);
|
|
1984
|
-
setInput('hint', field.hint);
|
|
1985
|
-
setInput('required', !!field.required);
|
|
1986
|
-
setInput('readonly', !!field.readonly);
|
|
1987
|
-
setInput('disabled', this.disabled || !!field.disabled);
|
|
1988
|
-
setInput('pattern', field.pattern);
|
|
1989
|
-
setInput('rows', field.rows ?? 4);
|
|
1990
|
-
setInput('fullWidth', field.fullWidth !== false);
|
|
1991
|
-
setInput('error', this.error);
|
|
1992
|
-
setInput('actions', this.actions);
|
|
1993
|
-
const instance = this.componentRef.instance;
|
|
1994
|
-
instance.writeValue?.(this.value);
|
|
1995
|
-
instance.setDisabledState?.(this.disabled || !!field.disabled);
|
|
1996
|
-
}
|
|
1997
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1998
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiFormInputHostComponent, isStandalone: true, selector: "forms-form-input-host", inputs: { field: "field", value: "value", error: "error", disabled: "disabled", actions: "actions", options: "options" }, outputs: { valueChange: "valueChange", actionTriggered: "actionTriggered" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true });
|
|
1999
|
-
}
|
|
2000
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, decorators: [{
|
|
2001
|
-
type: Component,
|
|
2002
|
-
args: [{
|
|
2003
|
-
selector: 'forms-form-input-host',
|
|
2004
|
-
standalone: true,
|
|
2005
|
-
template: '',
|
|
2006
|
-
}]
|
|
2007
|
-
}], propDecorators: { field: [{
|
|
2008
|
-
type: Input
|
|
2009
|
-
}], value: [{
|
|
2010
|
-
type: Input
|
|
2011
|
-
}], error: [{
|
|
2012
|
-
type: Input
|
|
2013
|
-
}], disabled: [{
|
|
2014
|
-
type: Input
|
|
2015
|
-
}], actions: [{
|
|
2016
|
-
type: Input
|
|
2017
|
-
}], options: [{
|
|
2018
|
-
type: Input
|
|
2019
|
-
}], valueChange: [{
|
|
2020
|
-
type: Output
|
|
2021
|
-
}], actionTriggered: [{
|
|
2022
|
-
type: Output
|
|
2023
|
-
}] } });
|
|
2024
|
-
|
|
2025
|
-
class UiFileInputComponent {
|
|
1665
|
+
class InputFileComponent {
|
|
2026
1666
|
label;
|
|
2027
1667
|
hint;
|
|
2028
1668
|
accept;
|
|
@@ -2031,6 +1671,12 @@ class UiFileInputComponent {
|
|
|
2031
1671
|
value = null;
|
|
2032
1672
|
valueChange = new EventEmitter();
|
|
2033
1673
|
filesChange = new EventEmitter();
|
|
1674
|
+
onChange = () => undefined;
|
|
1675
|
+
onTouched = () => undefined;
|
|
1676
|
+
writeValue(value) { this.value = value; }
|
|
1677
|
+
registerOnChange(fn) { this.onChange = fn; }
|
|
1678
|
+
registerOnTouched(fn) { this.onTouched = fn; }
|
|
1679
|
+
setDisabledState(isDisabled) { this.disabled = isDisabled; }
|
|
2034
1680
|
get selectedFiles() {
|
|
2035
1681
|
if (this.value instanceof File)
|
|
2036
1682
|
return [this.value];
|
|
@@ -2040,6 +1686,8 @@ class UiFileInputComponent {
|
|
|
2040
1686
|
const input = event.target;
|
|
2041
1687
|
const files = Array.from(input.files ?? []);
|
|
2042
1688
|
const value = this.multiple ? files : (files[0] ?? null);
|
|
1689
|
+
this.value = value;
|
|
1690
|
+
this.onChange(value);
|
|
2043
1691
|
this.valueChange.emit(value);
|
|
2044
1692
|
this.filesChange.emit(files);
|
|
2045
1693
|
input.value = '';
|
|
@@ -2047,15 +1695,17 @@ class UiFileInputComponent {
|
|
|
2047
1695
|
remove(index) {
|
|
2048
1696
|
const files = this.selectedFiles.filter((_file, fileIndex) => fileIndex !== index);
|
|
2049
1697
|
const value = this.multiple ? files : (files[0] ?? null);
|
|
1698
|
+
this.value = value;
|
|
1699
|
+
this.onChange(value);
|
|
2050
1700
|
this.valueChange.emit(value);
|
|
2051
1701
|
this.filesChange.emit(files);
|
|
2052
1702
|
}
|
|
2053
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
2054
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1703
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputFileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1704
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputFileComponent, isStandalone: true, selector: "input-file", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputFileComponent), multi: true }], ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n (blur)=\"onTouched()\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
2055
1705
|
}
|
|
2056
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1706
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputFileComponent, decorators: [{
|
|
2057
1707
|
type: Component,
|
|
2058
|
-
args: [{ selector: '
|
|
1708
|
+
args: [{ selector: 'input-file', standalone: true, imports: [CommonModule], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputFileComponent), multi: true }], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n (blur)=\"onTouched()\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
|
|
2059
1709
|
}], propDecorators: { label: [{
|
|
2060
1710
|
type: Input
|
|
2061
1711
|
}], hint: [{
|
|
@@ -2074,7 +1724,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
2074
1724
|
type: Output
|
|
2075
1725
|
}] } });
|
|
2076
1726
|
|
|
2077
|
-
class
|
|
1727
|
+
class InputPhotoComponent {
|
|
2078
1728
|
http = inject(HttpClient);
|
|
2079
1729
|
label;
|
|
2080
1730
|
hint;
|
|
@@ -2090,6 +1740,15 @@ class UiPhotoInputComponent {
|
|
|
2090
1740
|
previews = [];
|
|
2091
1741
|
uploading = false;
|
|
2092
1742
|
fileTypes = new Map();
|
|
1743
|
+
onChange = () => undefined;
|
|
1744
|
+
onTouched = () => undefined;
|
|
1745
|
+
writeValue(value) {
|
|
1746
|
+
this.value = value;
|
|
1747
|
+
this.rebuildPreviews();
|
|
1748
|
+
}
|
|
1749
|
+
registerOnChange(fn) { this.onChange = fn; }
|
|
1750
|
+
registerOnTouched(fn) { this.onTouched = fn; }
|
|
1751
|
+
setDisabledState(isDisabled) { this.disabled = isDisabled; }
|
|
2093
1752
|
ngOnChanges(changes) {
|
|
2094
1753
|
if (changes['value'] || changes['multiple'])
|
|
2095
1754
|
this.rebuildPreviews();
|
|
@@ -2140,6 +1799,8 @@ class UiPhotoInputComponent {
|
|
|
2140
1799
|
return Number.isInteger(Number(this.limit)) && Number(this.limit) > 0 ? Number(this.limit) : Number.MAX_SAFE_INTEGER;
|
|
2141
1800
|
}
|
|
2142
1801
|
emitValue(value) {
|
|
1802
|
+
this.value = value;
|
|
1803
|
+
this.onChange(value);
|
|
2143
1804
|
this.valueChange.emit(value);
|
|
2144
1805
|
this.rebuildPreviews(value);
|
|
2145
1806
|
}
|
|
@@ -2200,12 +1861,12 @@ class UiPhotoInputComponent {
|
|
|
2200
1861
|
const normalized = value.toLowerCase();
|
|
2201
1862
|
return normalized.startsWith('image/') || normalized === 'application/pdf';
|
|
2202
1863
|
}
|
|
2203
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
2204
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
1864
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1865
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputPhotoComponent, isStandalone: true, selector: "input-photo", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", limit: "limit", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange", uploadingChange: "uploadingChange" }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputPhotoComponent), multi: true }], usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
|
|
2205
1866
|
}
|
|
2206
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
1867
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoComponent, decorators: [{
|
|
2207
1868
|
type: Component,
|
|
2208
|
-
args: [{ selector: '
|
|
1869
|
+
args: [{ selector: 'input-photo', standalone: true, imports: [CommonModule, DragDropModule], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputPhotoComponent), multi: true }], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
|
|
2209
1870
|
}], propDecorators: { label: [{
|
|
2210
1871
|
type: Input
|
|
2211
1872
|
}], hint: [{
|
|
@@ -2228,53 +1889,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
2228
1889
|
type: Output
|
|
2229
1890
|
}] } });
|
|
2230
1891
|
|
|
2231
|
-
class
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
valueChange = new EventEmitter();
|
|
2236
|
-
onValueChange(event) {
|
|
2237
|
-
this.valueChange.emit(event.target.checked);
|
|
2238
|
-
}
|
|
2239
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2240
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiCheckboxInputComponent, isStandalone: true, selector: "forms-checkbox-input", inputs: { label: "label", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n />\n <span>{{ label }}</span>\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
2241
|
-
}
|
|
2242
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, decorators: [{
|
|
2243
|
-
type: Component,
|
|
2244
|
-
args: [{ selector: 'forms-checkbox-input', standalone: true, imports: [CommonModule], template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n />\n <span>{{ label }}</span>\n</label>\n" }]
|
|
2245
|
-
}], propDecorators: { label: [{
|
|
2246
|
-
type: Input
|
|
2247
|
-
}], disabled: [{
|
|
2248
|
-
type: Input
|
|
2249
|
-
}], value: [{
|
|
2250
|
-
type: Input
|
|
2251
|
-
}], valueChange: [{
|
|
2252
|
-
type: Output
|
|
2253
|
-
}] } });
|
|
2254
|
-
|
|
2255
|
-
class UiColorInputComponent {
|
|
1892
|
+
class InputSelectComponent {
|
|
1893
|
+
closeIcon = X;
|
|
1894
|
+
chevronDownIcon = ChevronDown;
|
|
1895
|
+
checkIcon = Check;
|
|
2256
1896
|
cdr = inject(ChangeDetectorRef);
|
|
1897
|
+
destroyRef = inject(DestroyRef);
|
|
1898
|
+
http = inject(HttpClient);
|
|
1899
|
+
popupService = inject(UiPopupService);
|
|
1900
|
+
closeActivePopup = () => this.closeDropdown();
|
|
2257
1901
|
label;
|
|
2258
|
-
|
|
1902
|
+
placeholder;
|
|
2259
1903
|
name;
|
|
2260
|
-
|
|
1904
|
+
data;
|
|
2261
1905
|
required = false;
|
|
2262
1906
|
fullWidth = false;
|
|
2263
|
-
error;
|
|
2264
1907
|
disabled = false;
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
1908
|
+
multiple = false;
|
|
1909
|
+
includeBlankOption = false;
|
|
1910
|
+
blankOptionLabel = 'Оберіть…';
|
|
1911
|
+
emptyStateLabel = 'Немає варіантів для вибору';
|
|
1912
|
+
searchable = true;
|
|
1913
|
+
remoteSearch = false;
|
|
1914
|
+
searchPlaceholder = 'Пошук...';
|
|
1915
|
+
_options = [];
|
|
1916
|
+
set options(value) {
|
|
1917
|
+
this._options = value ?? [];
|
|
1918
|
+
this.updateSelectionFromValue();
|
|
1919
|
+
this.cdr.markForCheck();
|
|
1920
|
+
}
|
|
1921
|
+
get options() {
|
|
1922
|
+
return this._options;
|
|
1923
|
+
}
|
|
1924
|
+
error;
|
|
1925
|
+
hint;
|
|
1926
|
+
triggerClass = '';
|
|
1927
|
+
panelClass = '';
|
|
1928
|
+
valueChange = new EventEmitter();
|
|
1929
|
+
searchChange = new EventEmitter();
|
|
1930
|
+
popupMinWidth = 300;
|
|
1931
|
+
popupViewportMargin = 16;
|
|
1932
|
+
popupOffset = 8;
|
|
1933
|
+
dropdownTrigger;
|
|
1934
|
+
dropdownPositions = [
|
|
1935
|
+
{
|
|
1936
|
+
originX: 'start',
|
|
1937
|
+
originY: 'bottom',
|
|
1938
|
+
overlayX: 'start',
|
|
1939
|
+
overlayY: 'top',
|
|
1940
|
+
offsetY: this.popupOffset,
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
originX: 'start',
|
|
1944
|
+
originY: 'top',
|
|
1945
|
+
overlayX: 'start',
|
|
1946
|
+
overlayY: 'bottom',
|
|
1947
|
+
offsetY: -this.popupOffset,
|
|
1948
|
+
},
|
|
1949
|
+
];
|
|
1950
|
+
isOpen = false;
|
|
1951
|
+
popupWidth = 0;
|
|
1952
|
+
value = '';
|
|
1953
|
+
multiValue = [];
|
|
1954
|
+
selectedOptions = [];
|
|
1955
|
+
searchTerm = '';
|
|
1956
|
+
isDictionaryLoading = false;
|
|
1957
|
+
searchChanges$ = new Subject();
|
|
1958
|
+
dictionaryRequestVersions = new Map();
|
|
1959
|
+
dictionaryOptions = [];
|
|
2268
1960
|
onChange = () => undefined;
|
|
2269
1961
|
onTouched = () => undefined;
|
|
2270
|
-
|
|
2271
|
-
|
|
1962
|
+
constructor() {
|
|
1963
|
+
this.searchChanges$.pipe(debounceTime(300), distinctUntilChanged()).subscribe((next) => {
|
|
1964
|
+
this.searchTerm = next;
|
|
1965
|
+
if (this.getDictionaryName()) {
|
|
1966
|
+
this.loadDictionaryOptions(next);
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
this.updateSelectionFromValue();
|
|
1970
|
+
this.cdr.markForCheck();
|
|
1971
|
+
});
|
|
1972
|
+
this.destroyRef.onDestroy(() => {
|
|
1973
|
+
this.searchChanges$.complete();
|
|
1974
|
+
this.dictionaryRequestVersions.clear();
|
|
1975
|
+
});
|
|
2272
1976
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
1977
|
+
ngOnChanges(changes) {
|
|
1978
|
+
if ('data' in changes) {
|
|
1979
|
+
this.reloadDictionaryOptions();
|
|
1980
|
+
}
|
|
2275
1981
|
}
|
|
2276
1982
|
writeValue(value) {
|
|
2277
|
-
this.
|
|
1983
|
+
if (this.multiple) {
|
|
1984
|
+
this.multiValue = Array.isArray(value) ? value : [];
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
this.value = value ?? '';
|
|
1988
|
+
}
|
|
1989
|
+
this.updateSelectionFromValue();
|
|
1990
|
+
this.ensureSelectedDictionaryOptionsLoaded();
|
|
2278
1991
|
this.cdr.markForCheck();
|
|
2279
1992
|
}
|
|
2280
1993
|
registerOnChange(fn) {
|
|
@@ -2283,66 +1996,324 @@ class UiColorInputComponent {
|
|
|
2283
1996
|
registerOnTouched(fn) {
|
|
2284
1997
|
this.onTouched = fn;
|
|
2285
1998
|
}
|
|
1999
|
+
validate(control) { return validateFormControl(control, this.required); }
|
|
2286
2000
|
setDisabledState(isDisabled) {
|
|
2287
2001
|
this.disabled = isDisabled;
|
|
2288
2002
|
this.cdr.markForCheck();
|
|
2289
2003
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
this.
|
|
2004
|
+
toggleDropdown(event) {
|
|
2005
|
+
event.stopPropagation();
|
|
2006
|
+
if (this.disabled) {
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (this.isOpen) {
|
|
2010
|
+
this.closeDropdown();
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const fieldWidth = this.dropdownTrigger.nativeElement.getBoundingClientRect().width;
|
|
2014
|
+
this.popupWidth = Math.max(fieldWidth, this.popupMinWidth);
|
|
2015
|
+
this.isOpen = true;
|
|
2016
|
+
this.popupService.open(this.closeActivePopup);
|
|
2017
|
+
this.searchTerm = '';
|
|
2018
|
+
this.searchChanges$.next('');
|
|
2019
|
+
this.cdr.markForCheck();
|
|
2293
2020
|
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
this.
|
|
2021
|
+
selectOption(option, event) {
|
|
2022
|
+
event.stopPropagation();
|
|
2023
|
+
const key = this.getOptionValue(option);
|
|
2024
|
+
if (this.multiple) {
|
|
2025
|
+
const normalized = this.multiValue.includes(key)
|
|
2026
|
+
? this.multiValue.filter((item) => item !== key)
|
|
2027
|
+
: [...this.multiValue, key];
|
|
2028
|
+
this.multiValue = normalized;
|
|
2029
|
+
this.onChange(normalized);
|
|
2030
|
+
this.valueChange.emit(normalized);
|
|
2031
|
+
}
|
|
2032
|
+
else {
|
|
2033
|
+
const nextValue = this.value === key ? null : key;
|
|
2034
|
+
this.value = nextValue;
|
|
2035
|
+
this.onChange(nextValue);
|
|
2036
|
+
this.valueChange.emit(nextValue);
|
|
2037
|
+
this.closeDropdown();
|
|
2038
|
+
}
|
|
2039
|
+
this.updateSelectionFromValue();
|
|
2297
2040
|
}
|
|
2298
2041
|
markTouched() {
|
|
2299
2042
|
this.onTouched();
|
|
2300
2043
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2044
|
+
closeDropdown() {
|
|
2045
|
+
if (!this.isOpen) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
this.isOpen = false;
|
|
2049
|
+
this.onTouched();
|
|
2050
|
+
this.searchTerm = '';
|
|
2051
|
+
this.searchChanges$.next('');
|
|
2052
|
+
this.popupService.close(this.closeActivePopup);
|
|
2053
|
+
this.cdr.markForCheck();
|
|
2054
|
+
}
|
|
2055
|
+
onDocumentClick() {
|
|
2056
|
+
if (this.isOpen) {
|
|
2057
|
+
this.closeDropdown();
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
onEscape() {
|
|
2061
|
+
if (this.isOpen) {
|
|
2062
|
+
this.closeDropdown();
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
get displayLabel() {
|
|
2066
|
+
if (this.multiple) {
|
|
2067
|
+
if (!this.selectedOptions.length) {
|
|
2068
|
+
return this.placeholder ?? this.blankOptionLabel;
|
|
2069
|
+
}
|
|
2070
|
+
return `${this.selectedOptions.length}`;
|
|
2071
|
+
}
|
|
2072
|
+
return this.selectedOptions[0]?.label ?? this.placeholder ?? this.blankOptionLabel;
|
|
2073
|
+
}
|
|
2074
|
+
isSelected(option) {
|
|
2075
|
+
const value = this.getOptionValue(option);
|
|
2076
|
+
if (this.multiple) {
|
|
2077
|
+
return this.multiValue.includes(value);
|
|
2078
|
+
}
|
|
2079
|
+
return this.value === value;
|
|
2080
|
+
}
|
|
2081
|
+
getOptionValue(option) {
|
|
2082
|
+
return option.value ?? '';
|
|
2083
|
+
}
|
|
2084
|
+
trackOption(index, option) {
|
|
2085
|
+
return `${this.getOptionValue(option)}::${String(option.label || '').trim()}::${index}`;
|
|
2086
|
+
}
|
|
2087
|
+
get visibleOptions() {
|
|
2088
|
+
const options = this.getRenderOptions();
|
|
2089
|
+
if (!this.searchable || this.remoteSearch || !this.searchTerm.trim()) {
|
|
2090
|
+
return options;
|
|
2091
|
+
}
|
|
2092
|
+
const search = this.searchTerm.trim().toLowerCase();
|
|
2093
|
+
return options.filter((option) => {
|
|
2094
|
+
if (this.includeBlankOption && !this.multiple && this.isBlankOption(option)) {
|
|
2095
|
+
return true;
|
|
2096
|
+
}
|
|
2097
|
+
return String(option.label ?? '').toLowerCase().includes(search);
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
onSearchChange(value) {
|
|
2101
|
+
const next = String(value ?? '');
|
|
2102
|
+
this.searchChanges$.next(next);
|
|
2103
|
+
this.searchChange.emit(next);
|
|
2104
|
+
}
|
|
2105
|
+
removeOption(option, event) {
|
|
2106
|
+
event.stopPropagation();
|
|
2107
|
+
const key = this.getOptionValue(option);
|
|
2108
|
+
if (this.multiple) {
|
|
2109
|
+
this.multiValue = this.multiValue.filter((v) => v !== key);
|
|
2110
|
+
this.onChange(this.multiValue);
|
|
2111
|
+
this.valueChange.emit(this.multiValue);
|
|
2112
|
+
this.updateSelectionFromValue();
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
updateSelectionFromValue() {
|
|
2116
|
+
const options = this.getRenderOptions();
|
|
2117
|
+
if (this.multiple) {
|
|
2118
|
+
this.selectedOptions = options.filter((option) => this.multiValue.includes(this.getOptionValue(option)));
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const current = options.find((option) => this.getOptionValue(option) === (this.value ?? ''));
|
|
2122
|
+
this.selectedOptions = current ? [current] : [];
|
|
2123
|
+
}
|
|
2124
|
+
getRenderOptions() {
|
|
2125
|
+
const merged = this.mergeOptions(this.options, this.dictionaryOptions);
|
|
2126
|
+
if (!this.includeBlankOption || this.multiple) {
|
|
2127
|
+
return merged;
|
|
2128
|
+
}
|
|
2129
|
+
return [{ label: this.blankOptionLabel, value: '' }, ...merged];
|
|
2130
|
+
}
|
|
2131
|
+
mergeOptions(...optionSets) {
|
|
2132
|
+
const merged = new Map();
|
|
2133
|
+
for (const options of optionSets) {
|
|
2134
|
+
for (const option of options || []) {
|
|
2135
|
+
const value = this.getOptionValue(option);
|
|
2136
|
+
const key = `${value}::${String(option.label || '').trim()}`;
|
|
2137
|
+
if (!merged.has(key)) {
|
|
2138
|
+
merged.set(key, option);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return [...merged.values()];
|
|
2143
|
+
}
|
|
2144
|
+
isBlankOption(option) {
|
|
2145
|
+
return String(option.value ?? '') === '';
|
|
2146
|
+
}
|
|
2147
|
+
getDictionaryName() {
|
|
2148
|
+
return String(this.data || '').trim();
|
|
2149
|
+
}
|
|
2150
|
+
reloadDictionaryOptions() {
|
|
2151
|
+
const dictionaryName = this.getDictionaryName();
|
|
2152
|
+
this.dictionaryRequestVersions.clear();
|
|
2153
|
+
this.dictionaryOptions = [];
|
|
2154
|
+
this.isDictionaryLoading = false;
|
|
2155
|
+
if (!dictionaryName) {
|
|
2156
|
+
this.updateSelectionFromValue();
|
|
2157
|
+
this.cdr.markForCheck();
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
this.loadDictionaryOptions(this.searchTerm);
|
|
2161
|
+
}
|
|
2162
|
+
loadDictionaryOptions(query) {
|
|
2163
|
+
const dictionaryName = this.getDictionaryName();
|
|
2164
|
+
if (!dictionaryName) {
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const requestVersion = (this.dictionaryRequestVersions.get(dictionaryName) || 0) + 1;
|
|
2168
|
+
this.dictionaryRequestVersions.set(dictionaryName, requestVersion);
|
|
2169
|
+
this.isDictionaryLoading = true;
|
|
2170
|
+
this.loadSuggestItems(dictionaryName, String(query || '').trim()).subscribe({
|
|
2171
|
+
next: (items) => {
|
|
2172
|
+
if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
this.dictionaryOptions = this.normalizeSuggestItems(items);
|
|
2176
|
+
this.isDictionaryLoading = false;
|
|
2177
|
+
this.ensureSelectedDictionaryOptionsLoaded();
|
|
2178
|
+
this.updateSelectionFromValue();
|
|
2179
|
+
this.cdr.markForCheck();
|
|
2180
|
+
},
|
|
2181
|
+
error: () => {
|
|
2182
|
+
if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
this.dictionaryOptions = [];
|
|
2186
|
+
this.isDictionaryLoading = false;
|
|
2187
|
+
this.updateSelectionFromValue();
|
|
2188
|
+
this.cdr.markForCheck();
|
|
2189
|
+
},
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
ensureSelectedDictionaryOptionsLoaded() {
|
|
2193
|
+
const dictionaryName = this.getDictionaryName();
|
|
2194
|
+
if (!dictionaryName) {
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
const existingValues = new Set(this.getRenderOptions().map((option) => this.getOptionValue(option)));
|
|
2198
|
+
const selectedValues = this.multiple ? this.multiValue : [this.value];
|
|
2199
|
+
const missingValues = selectedValues
|
|
2200
|
+
.map((item) => String(item || '').trim())
|
|
2201
|
+
.filter((item) => !!item && !existingValues.has(item));
|
|
2202
|
+
if (!missingValues.length) {
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
forkJoin(missingValues.map((value) => this.loadSuggestItem(dictionaryName, value))).subscribe({
|
|
2206
|
+
next: (items) => {
|
|
2207
|
+
const nextOptions = [...this.dictionaryOptions];
|
|
2208
|
+
for (const item of items) {
|
|
2209
|
+
const normalized = this.normalizeSuggestItem(item);
|
|
2210
|
+
if (!nextOptions.some((option) => this.getOptionValue(option) === normalized.value)) {
|
|
2211
|
+
nextOptions.push(normalized);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
this.dictionaryOptions = nextOptions;
|
|
2215
|
+
this.updateSelectionFromValue();
|
|
2216
|
+
this.cdr.markForCheck();
|
|
2217
|
+
},
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
normalizeSuggestItems(items) {
|
|
2221
|
+
return (items || [])
|
|
2222
|
+
.map((item) => this.normalizeSuggestItem(item))
|
|
2223
|
+
.filter((item) => !!item.label || item.value !== null);
|
|
2224
|
+
}
|
|
2225
|
+
normalizeSuggestItem(item) {
|
|
2226
|
+
return {
|
|
2227
|
+
label: String(item?.label ?? ''),
|
|
2228
|
+
value: item?.key != null ? String(item.key) : null,
|
|
2229
|
+
imageUrl: item?.imageId ? `/api/files/${item.imageId}/preview?scale=25` : undefined,
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
loadSuggestItems(name, query) {
|
|
2233
|
+
const params = new URLSearchParams();
|
|
2234
|
+
if (query?.trim())
|
|
2235
|
+
params.set('q', query.trim());
|
|
2236
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
2237
|
+
return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
|
|
2307
2238
|
}
|
|
2308
|
-
|
|
2309
|
-
return
|
|
2239
|
+
loadSuggestItem(name, key) {
|
|
2240
|
+
return this.http.get(`/api/suggest/${encodeURIComponent(name)}/${encodeURIComponent(key)}`);
|
|
2310
2241
|
}
|
|
2311
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
2312
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
2242
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2243
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputSelectComponent, isStandalone: true, selector: "input-select", inputs: { label: "label", placeholder: "placeholder", name: "name", data: "data", required: "required", fullWidth: "fullWidth", disabled: "disabled", multiple: ["multiple", "multiple", booleanAttribute], includeBlankOption: "includeBlankOption", blankOptionLabel: "blankOptionLabel", emptyStateLabel: "emptyStateLabel", searchable: "searchable", remoteSearch: "remoteSearch", searchPlaceholder: "searchPlaceholder", options: "options", error: "error", hint: "hint", triggerClass: "triggerClass", panelClass: "panelClass" }, outputs: { valueChange: "valueChange", searchChange: "searchChange" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()" } }, providers: [
|
|
2313
2244
|
{
|
|
2314
2245
|
provide: NG_VALUE_ACCESSOR,
|
|
2315
|
-
useExisting: forwardRef(() =>
|
|
2246
|
+
useExisting: forwardRef(() => InputSelectComponent),
|
|
2316
2247
|
multi: true,
|
|
2317
2248
|
},
|
|
2318
|
-
|
|
2249
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSelectComponent), multi: true },
|
|
2250
|
+
], viewQueries: [{ propertyName: "dropdownTrigger", first: true, predicate: ["dropdownTrigger"], descendants: true, read: ElementRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex min-h-10 w-full flex-nowrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2\" [class.-ml-2]=\"multiple\">\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md bg-slate-100 px-2 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n (keydown.enter)=\"removeOption(option, $event)\"\n (keydown.space)=\"$event.preventDefault(); removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"h-4 w-4 shrink-0\"></lucide-icon>\n </span>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"min-w-0 truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n </span>\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"shrink-0 text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"false\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayViewportMargin]=\"popupViewportMargin\"\n [cdkConnectedOverlayFlexibleDimensions]=\"true\"\n [cdkConnectedOverlayPush]=\"true\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 w-full max-w-[calc(100vw-2rem)] rounded-xl border border-slate-200 bg-white shadow-lg\"\n [style.width.px]=\"popupWidth\"\n [style.max-width]=\"'calc(100vw - 2rem)'\"\n [class]=\"panelClass\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-900 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n <div class=\"max-h-60 overflow-auto\">\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-start justify-between gap-3 whitespace-normal break-words px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"whitespace-normal break-words\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n </div>\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n", dependencies: [{ kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2319
2251
|
}
|
|
2320
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
2252
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSelectComponent, decorators: [{
|
|
2321
2253
|
type: Component,
|
|
2322
|
-
args: [{ selector: '
|
|
2254
|
+
args: [{ selector: 'input-select', standalone: true, imports: [LucideAngularModule, CommonModule, FormsModule, CdkConnectedOverlay, CdkOverlayOrigin], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
2323
2255
|
{
|
|
2324
2256
|
provide: NG_VALUE_ACCESSOR,
|
|
2325
|
-
useExisting: forwardRef(() =>
|
|
2257
|
+
useExisting: forwardRef(() => InputSelectComponent),
|
|
2326
2258
|
multi: true,
|
|
2327
2259
|
},
|
|
2328
|
-
|
|
2329
|
-
}]
|
|
2260
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSelectComponent), multi: true },
|
|
2261
|
+
], template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex min-h-10 w-full flex-nowrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <span class=\"flex min-w-0 flex-1 flex-wrap items-center gap-2\" [class.-ml-2]=\"multiple\">\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md bg-slate-100 px-2 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n (keydown.enter)=\"removeOption(option, $event)\"\n (keydown.space)=\"$event.preventDefault(); removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"h-4 w-4 shrink-0\"></lucide-icon>\n </span>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"min-w-0 truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n </span>\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"shrink-0 text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"false\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayViewportMargin]=\"popupViewportMargin\"\n [cdkConnectedOverlayFlexibleDimensions]=\"true\"\n [cdkConnectedOverlayPush]=\"true\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 w-full max-w-[calc(100vw-2rem)] rounded-xl border border-slate-200 bg-white shadow-lg\"\n [style.width.px]=\"popupWidth\"\n [style.max-width]=\"'calc(100vw - 2rem)'\"\n [class]=\"panelClass\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-900 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n <div class=\"max-h-60 overflow-auto\">\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-start justify-between gap-3 whitespace-normal break-words px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 flex-1 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"whitespace-normal break-words\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n </div>\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n" }]
|
|
2262
|
+
}], ctorParameters: () => [], propDecorators: { label: [{
|
|
2330
2263
|
type: Input
|
|
2331
|
-
}],
|
|
2264
|
+
}], placeholder: [{
|
|
2332
2265
|
type: Input
|
|
2333
2266
|
}], name: [{
|
|
2334
2267
|
type: Input
|
|
2335
|
-
}],
|
|
2268
|
+
}], data: [{
|
|
2336
2269
|
type: Input
|
|
2337
2270
|
}], required: [{
|
|
2338
2271
|
type: Input
|
|
2339
2272
|
}], fullWidth: [{
|
|
2340
2273
|
type: Input
|
|
2274
|
+
}], disabled: [{
|
|
2275
|
+
type: Input
|
|
2276
|
+
}], multiple: [{
|
|
2277
|
+
type: Input,
|
|
2278
|
+
args: [{ transform: booleanAttribute }]
|
|
2279
|
+
}], includeBlankOption: [{
|
|
2280
|
+
type: Input
|
|
2281
|
+
}], blankOptionLabel: [{
|
|
2282
|
+
type: Input
|
|
2283
|
+
}], emptyStateLabel: [{
|
|
2284
|
+
type: Input
|
|
2285
|
+
}], searchable: [{
|
|
2286
|
+
type: Input
|
|
2287
|
+
}], remoteSearch: [{
|
|
2288
|
+
type: Input
|
|
2289
|
+
}], searchPlaceholder: [{
|
|
2290
|
+
type: Input
|
|
2291
|
+
}], options: [{
|
|
2292
|
+
type: Input
|
|
2341
2293
|
}], error: [{
|
|
2342
2294
|
type: Input
|
|
2295
|
+
}], hint: [{
|
|
2296
|
+
type: Input
|
|
2297
|
+
}], triggerClass: [{
|
|
2298
|
+
type: Input
|
|
2299
|
+
}], panelClass: [{
|
|
2300
|
+
type: Input
|
|
2301
|
+
}], valueChange: [{
|
|
2302
|
+
type: Output
|
|
2303
|
+
}], searchChange: [{
|
|
2304
|
+
type: Output
|
|
2305
|
+
}], dropdownTrigger: [{
|
|
2306
|
+
type: ViewChild,
|
|
2307
|
+
args: ['dropdownTrigger', { static: true, read: ElementRef }]
|
|
2308
|
+
}], onDocumentClick: [{
|
|
2309
|
+
type: HostListener,
|
|
2310
|
+
args: ['document:click']
|
|
2311
|
+
}], onEscape: [{
|
|
2312
|
+
type: HostListener,
|
|
2313
|
+
args: ['document:keydown.escape']
|
|
2343
2314
|
}] } });
|
|
2344
2315
|
|
|
2345
|
-
class
|
|
2316
|
+
class InputSlugComponent {
|
|
2346
2317
|
label;
|
|
2347
2318
|
hint;
|
|
2348
2319
|
placeholder;
|
|
@@ -2351,13 +2322,12 @@ class UiSlugInputComponent {
|
|
|
2351
2322
|
required = false;
|
|
2352
2323
|
fullWidth = false;
|
|
2353
2324
|
autocomplete = 'off';
|
|
2354
|
-
readonly = false;
|
|
2355
2325
|
error;
|
|
2356
2326
|
invalidInput = new EventEmitter();
|
|
2357
2327
|
disabled = false;
|
|
2358
2328
|
value = '';
|
|
2359
2329
|
static nextId = 0;
|
|
2360
|
-
generatedId = `
|
|
2330
|
+
generatedId = `input-slug-${InputSlugComponent.nextId++}`;
|
|
2361
2331
|
onChange = () => undefined;
|
|
2362
2332
|
onTouched = () => undefined;
|
|
2363
2333
|
get controlId() {
|
|
@@ -2372,6 +2342,7 @@ class UiSlugInputComponent {
|
|
|
2372
2342
|
registerOnTouched(fn) {
|
|
2373
2343
|
this.onTouched = fn;
|
|
2374
2344
|
}
|
|
2345
|
+
validate(control) { return validateFormControl(control, this.required); }
|
|
2375
2346
|
setDisabledState(isDisabled) {
|
|
2376
2347
|
this.disabled = isDisabled;
|
|
2377
2348
|
}
|
|
@@ -2390,24 +2361,26 @@ class UiSlugInputComponent {
|
|
|
2390
2361
|
.replace(/\s+/g, '_')
|
|
2391
2362
|
.replace(/[^a-z0-9_]/g, '');
|
|
2392
2363
|
}
|
|
2393
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
2394
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
2364
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSlugComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2365
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputSlugComponent, isStandalone: true, selector: "input-slug", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", error: "error" }, outputs: { invalidInput: "invalidInput" }, providers: [
|
|
2395
2366
|
{
|
|
2396
2367
|
provide: NG_VALUE_ACCESSOR,
|
|
2397
|
-
useExisting: forwardRef(() =>
|
|
2368
|
+
useExisting: forwardRef(() => InputSlugComponent),
|
|
2398
2369
|
multi: true,
|
|
2399
2370
|
},
|
|
2400
|
-
|
|
2371
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSlugComponent), multi: true },
|
|
2372
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2401
2373
|
}
|
|
2402
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
2374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputSlugComponent, decorators: [{
|
|
2403
2375
|
type: Component,
|
|
2404
|
-
args: [{ selector: '
|
|
2376
|
+
args: [{ selector: 'input-slug', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
2405
2377
|
{
|
|
2406
2378
|
provide: NG_VALUE_ACCESSOR,
|
|
2407
|
-
useExisting: forwardRef(() =>
|
|
2379
|
+
useExisting: forwardRef(() => InputSlugComponent),
|
|
2408
2380
|
multi: true,
|
|
2409
2381
|
},
|
|
2410
|
-
|
|
2382
|
+
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputSlugComponent), multi: true },
|
|
2383
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [disabled]=\"disabled\"\n class=\"rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n" }]
|
|
2411
2384
|
}], propDecorators: { label: [{
|
|
2412
2385
|
type: Input
|
|
2413
2386
|
}], hint: [{
|
|
@@ -2424,19 +2397,439 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
2424
2397
|
type: Input
|
|
2425
2398
|
}], autocomplete: [{
|
|
2426
2399
|
type: Input
|
|
2427
|
-
}], readonly: [{
|
|
2428
|
-
type: Input
|
|
2429
2400
|
}], error: [{
|
|
2430
2401
|
type: Input
|
|
2431
2402
|
}], invalidInput: [{
|
|
2432
2403
|
type: Output
|
|
2433
2404
|
}] } });
|
|
2434
2405
|
|
|
2435
|
-
|
|
2436
|
-
|
|
2406
|
+
const UI_FORM_INPUT_REGISTRY = {
|
|
2407
|
+
text: InputTextComponent,
|
|
2408
|
+
number: InputNumberComponent,
|
|
2409
|
+
date: InputDateComponent,
|
|
2410
|
+
datetime: InputDatetimeComponent,
|
|
2411
|
+
email: InputEmailComponent,
|
|
2412
|
+
password: InputPasswordComponent,
|
|
2413
|
+
textarea: InputTextareaComponent,
|
|
2414
|
+
select: InputSelectComponent,
|
|
2415
|
+
checkbox: InputCheckboxComponent,
|
|
2416
|
+
color: InputColorComponent,
|
|
2417
|
+
slug: InputSlugComponent,
|
|
2418
|
+
file: InputFileComponent,
|
|
2419
|
+
photo: InputPhotoComponent,
|
|
2420
|
+
barcode: InputBarcodeComponent,
|
|
2421
|
+
};
|
|
2422
|
+
|
|
2423
|
+
class FieldInputComponent {
|
|
2424
|
+
field;
|
|
2425
|
+
value;
|
|
2426
|
+
error = '';
|
|
2427
|
+
disabled = false;
|
|
2428
|
+
actions = [];
|
|
2429
|
+
valueChange = new EventEmitter();
|
|
2430
|
+
actionTriggered = new EventEmitter();
|
|
2431
|
+
lookupRequested = new EventEmitter();
|
|
2432
|
+
barcodeScanned = new EventEmitter();
|
|
2433
|
+
filesChange = new EventEmitter();
|
|
2434
|
+
uploadingChange = new EventEmitter();
|
|
2435
|
+
inputContainer;
|
|
2436
|
+
componentRef;
|
|
2437
|
+
fallbackViewContainer = inject(ViewContainerRef);
|
|
2438
|
+
viewReady = false;
|
|
2439
|
+
ngAfterViewInit() {
|
|
2440
|
+
this.viewReady = true;
|
|
2441
|
+
if (this.field && !this.componentRef)
|
|
2442
|
+
this.render();
|
|
2443
|
+
}
|
|
2444
|
+
ngOnChanges(changes) {
|
|
2445
|
+
if (changes['field']) {
|
|
2446
|
+
if (this.viewReady)
|
|
2447
|
+
this.render();
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
if (this.viewReady)
|
|
2451
|
+
this.syncInputs();
|
|
2452
|
+
}
|
|
2453
|
+
render() {
|
|
2454
|
+
const viewContainer = this.inputContainer || this.fallbackViewContainer;
|
|
2455
|
+
viewContainer.clear();
|
|
2456
|
+
const inputType = String(this.field?.type || 'text');
|
|
2457
|
+
const componentType = UI_FORM_INPUT_REGISTRY[inputType] || UI_FORM_INPUT_REGISTRY.text;
|
|
2458
|
+
this.componentRef = viewContainer.createComponent(componentType);
|
|
2459
|
+
const hostElement = this.componentRef.location.nativeElement;
|
|
2460
|
+
hostElement.style.display = 'block';
|
|
2461
|
+
hostElement.style.width = '100%';
|
|
2462
|
+
this.syncInputs();
|
|
2463
|
+
const instance = this.componentRef.instance;
|
|
2464
|
+
const hasCva = !!instance.registerOnChange;
|
|
2465
|
+
instance.registerOnChange?.((nextValue) => this.valueChange.emit(nextValue));
|
|
2466
|
+
instance.registerOnTouched?.(() => undefined);
|
|
2467
|
+
instance.actionTriggered?.subscribe((action) => this.actionTriggered.emit(action));
|
|
2468
|
+
if (!hasCva)
|
|
2469
|
+
instance.valueChange?.subscribe((value) => this.valueChange.emit(value));
|
|
2470
|
+
instance.scanned?.subscribe((value) => this.barcodeScanned.emit(value));
|
|
2471
|
+
instance.searchRequested?.subscribe(() => this.lookupRequested.emit());
|
|
2472
|
+
instance.filesChange?.subscribe((files) => this.filesChange.emit(files));
|
|
2473
|
+
instance.uploadingChange?.subscribe((uploading) => this.uploadingChange.emit(uploading));
|
|
2474
|
+
}
|
|
2475
|
+
syncInputs() {
|
|
2476
|
+
if (!this.componentRef || !this.field)
|
|
2477
|
+
return;
|
|
2478
|
+
const field = this.field;
|
|
2479
|
+
const setInput = (name, value) => this.componentRef?.setInput(name, value);
|
|
2480
|
+
const inputType = String(field.type || 'text');
|
|
2481
|
+
const fullWidth = field.fullWidth !== false;
|
|
2482
|
+
// Common presentation belongs to this wrapper, not to the concrete control.
|
|
2483
|
+
// Only pass inputs that are actually declared by the selected component.
|
|
2484
|
+
switch (inputType) {
|
|
2485
|
+
case 'text':
|
|
2486
|
+
case 'number':
|
|
2487
|
+
case 'date':
|
|
2488
|
+
case 'datetime':
|
|
2489
|
+
case 'email':
|
|
2490
|
+
case 'password':
|
|
2491
|
+
case 'textarea':
|
|
2492
|
+
setInput('name', field.id);
|
|
2493
|
+
setInput('id', field.id);
|
|
2494
|
+
setInput('placeholder', field.placeholder);
|
|
2495
|
+
setInput('pattern', field.pattern);
|
|
2496
|
+
setInput('rows', field.rows ?? 4);
|
|
2497
|
+
setInput('fullWidth', fullWidth);
|
|
2498
|
+
break;
|
|
2499
|
+
case 'slug':
|
|
2500
|
+
setInput('name', field.id);
|
|
2501
|
+
setInput('id', field.id);
|
|
2502
|
+
setInput('placeholder', field.placeholder);
|
|
2503
|
+
setInput('fullWidth', fullWidth);
|
|
2504
|
+
break;
|
|
2505
|
+
case 'select':
|
|
2506
|
+
setInput('name', field.id);
|
|
2507
|
+
setInput('placeholder', field.placeholder);
|
|
2508
|
+
setInput('data', field.data);
|
|
2509
|
+
setInput('options', field.options || []);
|
|
2510
|
+
setInput('multiple', !!field.multiple);
|
|
2511
|
+
setInput('searchable', field.searchable ?? !!field.data);
|
|
2512
|
+
setInput('remoteSearch', !!field.data);
|
|
2513
|
+
setInput('searchPlaceholder', field.placeholder || 'Пошук...');
|
|
2514
|
+
setInput('fullWidth', fullWidth);
|
|
2515
|
+
break;
|
|
2516
|
+
case 'color':
|
|
2517
|
+
setInput('name', field.id);
|
|
2518
|
+
setInput('id', field.id);
|
|
2519
|
+
setInput('required', false);
|
|
2520
|
+
setInput('fullWidth', fullWidth);
|
|
2521
|
+
break;
|
|
2522
|
+
case 'file':
|
|
2523
|
+
setInput('accept', field.accept);
|
|
2524
|
+
setInput('multiple', !!field.multiple);
|
|
2525
|
+
break;
|
|
2526
|
+
case 'photo':
|
|
2527
|
+
setInput('accept', field.accept || 'image/*');
|
|
2528
|
+
setInput('multiple', !!field.multiple);
|
|
2529
|
+
setInput('limit', field.limit ?? 0);
|
|
2530
|
+
break;
|
|
2531
|
+
case 'barcode':
|
|
2532
|
+
setInput('label', '');
|
|
2533
|
+
setInput('hint', '');
|
|
2534
|
+
setInput('placeholder', field.placeholder);
|
|
2535
|
+
setInput('disabled', this.disabled);
|
|
2536
|
+
setInput('autoOpen', field.autoOpen !== false);
|
|
2537
|
+
setInput('searchButtonLabel', field.searchButtonLabel || 'Знайти');
|
|
2538
|
+
setInput('openCameraLabel', field.openCameraLabel || 'Відкрити камеру');
|
|
2539
|
+
setInput('closeCameraLabel', field.closeCameraLabel || 'Закрити камеру');
|
|
2540
|
+
setInput('cameraTitle', field.cameraTitle || 'Камера');
|
|
2541
|
+
setInput('cameraStartingLabel', field.cameraStartingLabel || 'Запускаємо камеру…');
|
|
2542
|
+
setInput('cameraHint', field.cameraHint || 'Наведіть камеру на штрихкод. Після зчитування код підставиться автоматично.');
|
|
2543
|
+
setInput('value', this.value ?? '');
|
|
2544
|
+
break;
|
|
2545
|
+
case 'checkbox':
|
|
2546
|
+
break;
|
|
2547
|
+
}
|
|
2548
|
+
const instance = this.componentRef.instance;
|
|
2549
|
+
instance.writeValue?.(this.value);
|
|
2550
|
+
instance.setDisabledState?.(this.disabled);
|
|
2551
|
+
}
|
|
2552
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FieldInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2553
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FieldInputComponent, isStandalone: true, selector: "field-input", inputs: { field: "field", value: "value", error: "error", disabled: "disabled", actions: "actions" }, outputs: { valueChange: "valueChange", actionTriggered: "actionTriggered", lookupRequested: "lookupRequested", barcodeScanned: "barcodeScanned", filesChange: "filesChange", uploadingChange: "uploadingChange" }, viewQueries: [{ propertyName: "inputContainer", first: true, predicate: ["inputContainer"], descendants: true, read: ViewContainerRef }], usesOnChanges: true, ngImport: i0, template: `
|
|
2554
|
+
<div class="flex flex-col gap-1" [class.w-full]="field.fullWidth !== false">
|
|
2555
|
+
@if (field.label) {
|
|
2556
|
+
<label class="text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600" [attr.for]="field.id">
|
|
2557
|
+
{{ field.label }}
|
|
2558
|
+
@if (field.required) { <span aria-hidden="true" class="text-rose-500"> *</span> }
|
|
2559
|
+
</label>
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
<div class="flex items-start gap-2" [class.rounded-xl]="!!error" [class.ring-2]="!!error" [class.ring-rose-200]="!!error">
|
|
2563
|
+
<div class="min-w-0 w-full flex-1">
|
|
2564
|
+
<ng-container #inputContainer></ng-container>
|
|
2565
|
+
</div>
|
|
2566
|
+
|
|
2567
|
+
@if (actions.length) {
|
|
2568
|
+
<div class="flex flex-none items-center gap-2 pt-0.5">
|
|
2569
|
+
@for (action of actions; track action.id) {
|
|
2570
|
+
<ui-button
|
|
2571
|
+
type="button"
|
|
2572
|
+
[variant]="action.variant ?? 'secondary'"
|
|
2573
|
+
[icon]="action.icon || ''"
|
|
2574
|
+
[size]="action.size || 'sm'"
|
|
2575
|
+
[disabled]="disabled"
|
|
2576
|
+
(click)="actionTriggered.emit(action)"
|
|
2577
|
+
>
|
|
2578
|
+
{{ action.label }}
|
|
2579
|
+
</ui-button>
|
|
2580
|
+
}
|
|
2581
|
+
</div>
|
|
2582
|
+
}
|
|
2583
|
+
</div>
|
|
2584
|
+
|
|
2585
|
+
@if (field.hint) { <p class="text-[0.5rem] text-slate-500">{{ field.hint }}</p> }
|
|
2586
|
+
@if (error) { <p class="text-[0.65rem] text-rose-500">{{ error }}</p> }
|
|
2587
|
+
</div>
|
|
2588
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }] });
|
|
2589
|
+
}
|
|
2590
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FieldInputComponent, decorators: [{
|
|
2591
|
+
type: Component,
|
|
2592
|
+
args: [{
|
|
2593
|
+
selector: 'field-input',
|
|
2594
|
+
standalone: true,
|
|
2595
|
+
imports: [CommonModule, UiButtonComponent],
|
|
2596
|
+
template: `
|
|
2597
|
+
<div class="flex flex-col gap-1" [class.w-full]="field.fullWidth !== false">
|
|
2598
|
+
@if (field.label) {
|
|
2599
|
+
<label class="text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600" [attr.for]="field.id">
|
|
2600
|
+
{{ field.label }}
|
|
2601
|
+
@if (field.required) { <span aria-hidden="true" class="text-rose-500"> *</span> }
|
|
2602
|
+
</label>
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
<div class="flex items-start gap-2" [class.rounded-xl]="!!error" [class.ring-2]="!!error" [class.ring-rose-200]="!!error">
|
|
2606
|
+
<div class="min-w-0 w-full flex-1">
|
|
2607
|
+
<ng-container #inputContainer></ng-container>
|
|
2608
|
+
</div>
|
|
2609
|
+
|
|
2610
|
+
@if (actions.length) {
|
|
2611
|
+
<div class="flex flex-none items-center gap-2 pt-0.5">
|
|
2612
|
+
@for (action of actions; track action.id) {
|
|
2613
|
+
<ui-button
|
|
2614
|
+
type="button"
|
|
2615
|
+
[variant]="action.variant ?? 'secondary'"
|
|
2616
|
+
[icon]="action.icon || ''"
|
|
2617
|
+
[size]="action.size || 'sm'"
|
|
2618
|
+
[disabled]="disabled"
|
|
2619
|
+
(click)="actionTriggered.emit(action)"
|
|
2620
|
+
>
|
|
2621
|
+
{{ action.label }}
|
|
2622
|
+
</ui-button>
|
|
2623
|
+
}
|
|
2624
|
+
</div>
|
|
2625
|
+
}
|
|
2626
|
+
</div>
|
|
2627
|
+
|
|
2628
|
+
@if (field.hint) { <p class="text-[0.5rem] text-slate-500">{{ field.hint }}</p> }
|
|
2629
|
+
@if (error) { <p class="text-[0.65rem] text-rose-500">{{ error }}</p> }
|
|
2630
|
+
</div>
|
|
2631
|
+
`,
|
|
2632
|
+
}]
|
|
2633
|
+
}], propDecorators: { field: [{
|
|
2634
|
+
type: Input
|
|
2635
|
+
}], value: [{
|
|
2636
|
+
type: Input
|
|
2637
|
+
}], error: [{
|
|
2638
|
+
type: Input
|
|
2639
|
+
}], disabled: [{
|
|
2640
|
+
type: Input
|
|
2641
|
+
}], actions: [{
|
|
2642
|
+
type: Input
|
|
2643
|
+
}], valueChange: [{
|
|
2644
|
+
type: Output
|
|
2645
|
+
}], actionTriggered: [{
|
|
2646
|
+
type: Output
|
|
2647
|
+
}], lookupRequested: [{
|
|
2648
|
+
type: Output
|
|
2649
|
+
}], barcodeScanned: [{
|
|
2650
|
+
type: Output
|
|
2651
|
+
}], filesChange: [{
|
|
2652
|
+
type: Output
|
|
2653
|
+
}], uploadingChange: [{
|
|
2654
|
+
type: Output
|
|
2655
|
+
}], inputContainer: [{
|
|
2656
|
+
type: ViewChild,
|
|
2657
|
+
args: ['inputContainer', { read: ViewContainerRef }]
|
|
2658
|
+
}] } });
|
|
2659
|
+
|
|
2660
|
+
const isIdentifierStart = (value) => /[A-Za-z_$]/.test(value);
|
|
2661
|
+
const isIdentifierPart = (value) => /[A-Za-z0-9_.$-]/.test(value);
|
|
2662
|
+
const tokenize = (source) => {
|
|
2663
|
+
const tokens = [];
|
|
2664
|
+
let index = 0;
|
|
2665
|
+
while (index < source.length) {
|
|
2666
|
+
const char = source[index];
|
|
2667
|
+
if (/\s/.test(char)) {
|
|
2668
|
+
index += 1;
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
const three = source.slice(index, index + 3);
|
|
2672
|
+
const two = source.slice(index, index + 2);
|
|
2673
|
+
if (three === '===' || three === '!==') {
|
|
2674
|
+
tokens.push({ type: 'operator', value: three });
|
|
2675
|
+
index += 3;
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
if (two === '==' || two === '!=' || two === '&&' || two === '||') {
|
|
2679
|
+
tokens.push({ type: 'operator', value: two });
|
|
2680
|
+
index += 2;
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
if (char === '!' || char === '(' || char === ')') {
|
|
2684
|
+
tokens.push({ type: 'operator', value: char });
|
|
2685
|
+
index += 1;
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
if (char === '"' || char === "'") {
|
|
2689
|
+
const quote = char;
|
|
2690
|
+
let value = '';
|
|
2691
|
+
let escaped = false;
|
|
2692
|
+
index += 1;
|
|
2693
|
+
while (index < source.length) {
|
|
2694
|
+
const current = source[index++];
|
|
2695
|
+
if (escaped) {
|
|
2696
|
+
value += current;
|
|
2697
|
+
escaped = false;
|
|
2698
|
+
}
|
|
2699
|
+
else if (current === '\\') {
|
|
2700
|
+
escaped = true;
|
|
2701
|
+
}
|
|
2702
|
+
else if (current === quote) {
|
|
2703
|
+
tokens.push({ type: 'value', value });
|
|
2704
|
+
break;
|
|
2705
|
+
}
|
|
2706
|
+
else {
|
|
2707
|
+
value += current;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
if (source[index - 1] !== quote)
|
|
2711
|
+
throw new Error('Unterminated string');
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
if (/[0-9.-]/.test(char)) {
|
|
2715
|
+
const match = source.slice(index).match(/^-?\d+(?:\.\d+)?/);
|
|
2716
|
+
if (match) {
|
|
2717
|
+
tokens.push({ type: 'value', value: Number(match[0]) });
|
|
2718
|
+
index += match[0].length;
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
if (isIdentifierStart(char)) {
|
|
2723
|
+
let value = char;
|
|
2724
|
+
index += 1;
|
|
2725
|
+
while (index < source.length && isIdentifierPart(source[index]))
|
|
2726
|
+
value += source[index++];
|
|
2727
|
+
if (value === 'true')
|
|
2728
|
+
tokens.push({ type: 'value', value: true });
|
|
2729
|
+
else if (value === 'false')
|
|
2730
|
+
tokens.push({ type: 'value', value: false });
|
|
2731
|
+
else if (value === 'null')
|
|
2732
|
+
tokens.push({ type: 'value', value: null });
|
|
2733
|
+
else
|
|
2734
|
+
tokens.push({ type: 'identifier', value });
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
throw new Error(`Unexpected token: ${char}`);
|
|
2738
|
+
}
|
|
2739
|
+
return tokens;
|
|
2740
|
+
};
|
|
2741
|
+
const getPathValue = (model, path) => {
|
|
2742
|
+
return path.split('.').reduce((current, part) => {
|
|
2743
|
+
if (current == null || typeof current !== 'object')
|
|
2744
|
+
return undefined;
|
|
2745
|
+
return current[part];
|
|
2746
|
+
}, model);
|
|
2747
|
+
};
|
|
2748
|
+
const evaluateFormCondition = (source, model) => {
|
|
2749
|
+
const tokens = tokenize(String(source || '').trim());
|
|
2750
|
+
let index = 0;
|
|
2751
|
+
const peek = () => tokens[index];
|
|
2752
|
+
const consume = () => {
|
|
2753
|
+
const token = tokens[index++];
|
|
2754
|
+
if (!token)
|
|
2755
|
+
throw new Error('Unexpected end of condition');
|
|
2756
|
+
return token;
|
|
2757
|
+
};
|
|
2758
|
+
const matchOperator = (...values) => {
|
|
2759
|
+
const token = peek();
|
|
2760
|
+
if (token?.type !== 'operator' || !values.includes(token.value))
|
|
2761
|
+
return false;
|
|
2762
|
+
index += 1;
|
|
2763
|
+
return true;
|
|
2764
|
+
};
|
|
2765
|
+
const toBoolean = (value) => typeof value === 'string' ? value.trim().length > 0 : Boolean(value);
|
|
2766
|
+
const parseExpression = () => parseOr();
|
|
2767
|
+
const parseOr = () => {
|
|
2768
|
+
let value = parseAnd();
|
|
2769
|
+
while (matchOperator('||'))
|
|
2770
|
+
value = toBoolean(value) || toBoolean(parseAnd());
|
|
2771
|
+
return value;
|
|
2772
|
+
};
|
|
2773
|
+
const parseAnd = () => {
|
|
2774
|
+
let value = parseEquality();
|
|
2775
|
+
while (matchOperator('&&'))
|
|
2776
|
+
value = toBoolean(value) && toBoolean(parseEquality());
|
|
2777
|
+
return value;
|
|
2778
|
+
};
|
|
2779
|
+
const parseEquality = () => {
|
|
2780
|
+
let value = parseUnary();
|
|
2781
|
+
while (true) {
|
|
2782
|
+
if (matchOperator('==')) {
|
|
2783
|
+
value = value == parseUnary();
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
if (matchOperator('!=')) {
|
|
2787
|
+
value = value != parseUnary();
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
if (matchOperator('===')) {
|
|
2791
|
+
value = value === parseUnary();
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (matchOperator('!==')) {
|
|
2795
|
+
value = value !== parseUnary();
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2798
|
+
return value;
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
const parseUnary = () => matchOperator('!') ? !toBoolean(parseUnary()) : parsePrimary();
|
|
2802
|
+
const parsePrimary = () => {
|
|
2803
|
+
if (matchOperator('(')) {
|
|
2804
|
+
const value = parseExpression();
|
|
2805
|
+
if (!matchOperator(')'))
|
|
2806
|
+
throw new Error('Missing closing parenthesis');
|
|
2807
|
+
return value;
|
|
2808
|
+
}
|
|
2809
|
+
const token = consume();
|
|
2810
|
+
if (token.type === 'value')
|
|
2811
|
+
return token.value;
|
|
2812
|
+
if (token.type !== 'identifier')
|
|
2813
|
+
throw new Error('Expected value');
|
|
2814
|
+
if (token.value === 'Number' && matchOperator('(')) {
|
|
2815
|
+
const value = Number(parseExpression());
|
|
2816
|
+
if (!matchOperator(')'))
|
|
2817
|
+
throw new Error('Missing closing parenthesis');
|
|
2818
|
+
return Number.isNaN(value) ? undefined : value;
|
|
2819
|
+
}
|
|
2820
|
+
return getPathValue(model, token.value);
|
|
2821
|
+
};
|
|
2822
|
+
if (!tokens.length)
|
|
2823
|
+
return true;
|
|
2824
|
+
const result = parseExpression();
|
|
2825
|
+
if (index !== tokens.length)
|
|
2826
|
+
throw new Error('Unexpected tokens after condition');
|
|
2827
|
+
return toBoolean(result);
|
|
2828
|
+
};
|
|
2829
|
+
|
|
2830
|
+
class DynamicFormComponent {
|
|
2437
2831
|
lookupService = inject(UiFormLookupService);
|
|
2438
2832
|
formContext = inject(UiFormContext);
|
|
2439
|
-
optionsSourceResolvers = inject(UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS, { optional: true }) ?? [];
|
|
2440
2833
|
fields = [];
|
|
2441
2834
|
value = {};
|
|
2442
2835
|
errors = {};
|
|
@@ -2450,15 +2843,8 @@ class UiDynamicFormComponent {
|
|
|
2450
2843
|
uploadingChange = new EventEmitter();
|
|
2451
2844
|
fieldActionTriggered = new EventEmitter();
|
|
2452
2845
|
modelSignal = signal({}, ...(ngDevMode ? [{ debugName: "modelSignal" }] : /* istanbul ignore next */ []));
|
|
2453
|
-
dictionaryOptionsSignal = signal({}, ...(ngDevMode ? [{ debugName: "dictionaryOptionsSignal" }] : /* istanbul ignore next */ []));
|
|
2454
2846
|
lookupLoadingSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupLoadingSignal" }] : /* istanbul ignore next */ []));
|
|
2455
2847
|
lookupErrorsSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupErrorsSignal" }] : /* istanbul ignore next */ []));
|
|
2456
|
-
selectSearchChanges = new Map();
|
|
2457
|
-
selectSearchSubscriptions = new Map();
|
|
2458
|
-
optionsSourceRequestVersions = new Map();
|
|
2459
|
-
ngOnInit() {
|
|
2460
|
-
this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
|
|
2461
|
-
}
|
|
2462
2848
|
ngOnChanges(changes) {
|
|
2463
2849
|
if ('fields' in changes || 'value' in changes) {
|
|
2464
2850
|
const next = { ...this.value };
|
|
@@ -2476,19 +2862,12 @@ class UiDynamicFormComponent {
|
|
|
2476
2862
|
this.formContext.setErrors(this.errors);
|
|
2477
2863
|
this.formContext.disabled.set(this.disabled);
|
|
2478
2864
|
}
|
|
2479
|
-
if ('fields' in changes || 'value' in changes) {
|
|
2480
|
-
this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
ngOnDestroy() {
|
|
2484
|
-
this.cleanupSelectSearchStreams();
|
|
2485
2865
|
}
|
|
2486
2866
|
updateField(fieldId, nextValue) {
|
|
2487
2867
|
this.modelSignal.update((current) => {
|
|
2488
2868
|
const updated = { ...current, [fieldId]: nextValue };
|
|
2489
2869
|
this.formContext.setValue(updated);
|
|
2490
2870
|
this.valueChange.emit(updated);
|
|
2491
|
-
this.ensureOptionsSourceOptionsLoaded(updated);
|
|
2492
2871
|
return updated;
|
|
2493
2872
|
});
|
|
2494
2873
|
}
|
|
@@ -2503,18 +2882,17 @@ class UiDynamicFormComponent {
|
|
|
2503
2882
|
this.updateField(field.id, value);
|
|
2504
2883
|
this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
|
|
2505
2884
|
}
|
|
2506
|
-
|
|
2885
|
+
onFieldValueChange(field, value) {
|
|
2507
2886
|
this.updateField(field.id, value);
|
|
2508
|
-
this.
|
|
2887
|
+
this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
|
|
2888
|
+
if (field.type === 'select' && field.lookupApi) {
|
|
2889
|
+
const lookupValue = Array.isArray(value) ? '' : String(value || '').trim();
|
|
2890
|
+
this.runFieldLookup(field, lookupValue);
|
|
2891
|
+
}
|
|
2509
2892
|
}
|
|
2510
|
-
|
|
2893
|
+
onBarcodeScanned(field, value) {
|
|
2511
2894
|
this.updateField(field.id, value);
|
|
2512
|
-
|
|
2513
|
-
if (field.lookupApi) {
|
|
2514
|
-
this.runFieldLookup(field, nextValue);
|
|
2515
|
-
return;
|
|
2516
|
-
}
|
|
2517
|
-
this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
|
|
2895
|
+
this.runFieldLookup(field, value);
|
|
2518
2896
|
}
|
|
2519
2897
|
runBarcodeLookup(field) {
|
|
2520
2898
|
this.runFieldLookup(field);
|
|
@@ -2561,8 +2939,7 @@ class UiDynamicFormComponent {
|
|
|
2561
2939
|
return true;
|
|
2562
2940
|
}
|
|
2563
2941
|
try {
|
|
2564
|
-
|
|
2565
|
-
return evaluator(model);
|
|
2942
|
+
return evaluateFormCondition(condition, model);
|
|
2566
2943
|
}
|
|
2567
2944
|
catch {
|
|
2568
2945
|
return true;
|
|
@@ -2582,7 +2959,7 @@ class UiDynamicFormComponent {
|
|
|
2582
2959
|
return false;
|
|
2583
2960
|
const condition = raw.startsWith('${') && raw.endsWith('}') ? raw.slice(2, -1).trim() : raw;
|
|
2584
2961
|
try {
|
|
2585
|
-
return
|
|
2962
|
+
return evaluateFormCondition(condition, this.modelSignal());
|
|
2586
2963
|
}
|
|
2587
2964
|
catch {
|
|
2588
2965
|
return false;
|
|
@@ -2594,13 +2971,6 @@ class UiDynamicFormComponent {
|
|
|
2594
2971
|
isTextArea(field) {
|
|
2595
2972
|
return (field.type ?? 'text') === 'textarea';
|
|
2596
2973
|
}
|
|
2597
|
-
isInputField(field) {
|
|
2598
|
-
const type = field.type ?? 'text';
|
|
2599
|
-
return type === 'text' || type === 'number' || type === 'date' || type === 'datetime' || type === 'email' || type === 'password' || type === 'textarea';
|
|
2600
|
-
}
|
|
2601
|
-
isBarcodeField(field) {
|
|
2602
|
-
return field.type === 'barcode';
|
|
2603
|
-
}
|
|
2604
2974
|
getStringValue(fieldId) {
|
|
2605
2975
|
return String(this.modelSignal()[fieldId] || '');
|
|
2606
2976
|
}
|
|
@@ -2632,8 +3002,7 @@ class UiDynamicFormComponent {
|
|
|
2632
3002
|
: hidden;
|
|
2633
3003
|
try {
|
|
2634
3004
|
const model = this.modelSignal();
|
|
2635
|
-
|
|
2636
|
-
return !evaluator(model);
|
|
3005
|
+
return !evaluateFormCondition(expression, model);
|
|
2637
3006
|
}
|
|
2638
3007
|
catch {
|
|
2639
3008
|
return false;
|
|
@@ -2648,16 +3017,6 @@ class UiDynamicFormComponent {
|
|
|
2648
3017
|
return 'datetime-local';
|
|
2649
3018
|
return 'text';
|
|
2650
3019
|
}
|
|
2651
|
-
resolveSelectOptions(field) {
|
|
2652
|
-
if (field.options?.length) {
|
|
2653
|
-
return field.options;
|
|
2654
|
-
}
|
|
2655
|
-
const sourceKey = String(field.optionsSource || '').trim();
|
|
2656
|
-
if (!sourceKey) {
|
|
2657
|
-
return [];
|
|
2658
|
-
}
|
|
2659
|
-
return this.dictionaryOptionsSignal()[sourceKey] ?? [];
|
|
2660
|
-
}
|
|
2661
3020
|
renderTemplate(field) {
|
|
2662
3021
|
const template = this.isTemplateField(field) ? field.template : '';
|
|
2663
3022
|
if (!template) {
|
|
@@ -2730,103 +3089,6 @@ class UiDynamicFormComponent {
|
|
|
2730
3089
|
};
|
|
2731
3090
|
return classes[span] || classes[12];
|
|
2732
3091
|
}
|
|
2733
|
-
onSelectSearchChange(field, query) {
|
|
2734
|
-
const key = String(field.optionsSource || '').trim();
|
|
2735
|
-
if (!key)
|
|
2736
|
-
return;
|
|
2737
|
-
this.getSelectSearchStream(field, key).next(String(query ?? ''));
|
|
2738
|
-
}
|
|
2739
|
-
ensureOptionsSourceOptionsLoaded(model) {
|
|
2740
|
-
const sourceKeys = [...new Set(this.getLeafFields()
|
|
2741
|
-
.filter((field) => (field.type ?? 'text') === 'select' && !!field.optionsSource)
|
|
2742
|
-
.map((field) => String(field.optionsSource || '').trim())
|
|
2743
|
-
.filter(Boolean))];
|
|
2744
|
-
if (!sourceKeys.length) {
|
|
2745
|
-
return;
|
|
2746
|
-
}
|
|
2747
|
-
for (const sourceKey of sourceKeys) {
|
|
2748
|
-
const resolver = this.optionsSourceResolvers.find((item) => item.source === sourceKey);
|
|
2749
|
-
if (!resolver) {
|
|
2750
|
-
continue;
|
|
2751
|
-
}
|
|
2752
|
-
const sourceFields = this.getLeafFields().filter((field) => String(field.optionsSource || '').trim() === sourceKey);
|
|
2753
|
-
if (!sourceFields.length) {
|
|
2754
|
-
continue;
|
|
2755
|
-
}
|
|
2756
|
-
const version = (this.optionsSourceRequestVersions.get(sourceKey) || 0) + 1;
|
|
2757
|
-
this.optionsSourceRequestVersions.set(sourceKey, version);
|
|
2758
|
-
forkJoin(sourceFields.map((field) => {
|
|
2759
|
-
const resolved = resolver
|
|
2760
|
-
? resolver.resolve({ source: sourceKey, field, model })
|
|
2761
|
-
: this.formsApi.getSuggestByName(sourceKey);
|
|
2762
|
-
return resolved instanceof Observable ? resolved : from(Promise.resolve(resolved));
|
|
2763
|
-
})).subscribe({
|
|
2764
|
-
next: (results) => {
|
|
2765
|
-
if ((this.optionsSourceRequestVersions.get(sourceKey) || 0) !== version) {
|
|
2766
|
-
return;
|
|
2767
|
-
}
|
|
2768
|
-
const options = results
|
|
2769
|
-
.flat()
|
|
2770
|
-
.map((item) => ({
|
|
2771
|
-
label: String(item?.label ?? ''),
|
|
2772
|
-
value: item?.value == null ? null : String(item.value),
|
|
2773
|
-
disabled: typeof item?.disabled === 'boolean' ? item.disabled : undefined,
|
|
2774
|
-
imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
|
|
2775
|
-
}))
|
|
2776
|
-
.filter((item) => !!item.label || item.value !== null);
|
|
2777
|
-
this.dictionaryOptionsSignal.update((current) => ({
|
|
2778
|
-
...current,
|
|
2779
|
-
[sourceKey]: options,
|
|
2780
|
-
}));
|
|
2781
|
-
},
|
|
2782
|
-
});
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
getSelectSearchStream(field, key) {
|
|
2786
|
-
const normalizedKey = String(key || '').trim();
|
|
2787
|
-
const existing = this.selectSearchChanges.get(normalizedKey);
|
|
2788
|
-
if (existing) {
|
|
2789
|
-
return existing;
|
|
2790
|
-
}
|
|
2791
|
-
const search$ = new Subject();
|
|
2792
|
-
this.selectSearchChanges.set(normalizedKey, search$);
|
|
2793
|
-
const subscription = search$
|
|
2794
|
-
.pipe(debounceTime(300), distinctUntilChanged(), switchMap((query) => {
|
|
2795
|
-
const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
|
|
2796
|
-
if (resolver) {
|
|
2797
|
-
return resolver.resolve({ source: normalizedKey, field, model: this.modelSignal(), query });
|
|
2798
|
-
}
|
|
2799
|
-
return this.formsApi.getSuggestByName(normalizedKey, query);
|
|
2800
|
-
}))
|
|
2801
|
-
.subscribe({
|
|
2802
|
-
next: (items) => {
|
|
2803
|
-
const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
|
|
2804
|
-
this.dictionaryOptionsSignal.update((current) => ({
|
|
2805
|
-
...current,
|
|
2806
|
-
[normalizedKey]: resolver
|
|
2807
|
-
? (items ?? []).map((item) => ({
|
|
2808
|
-
label: String(item?.label ?? ''),
|
|
2809
|
-
value: item?.value == null ? null : String(item.value),
|
|
2810
|
-
disabled: item?.disabled,
|
|
2811
|
-
imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
|
|
2812
|
-
}))
|
|
2813
|
-
: (items ?? []).map((item) => ({
|
|
2814
|
-
label: String(item?.label ?? ''),
|
|
2815
|
-
value: item?.['key'] != null ? String(item['key']) : null,
|
|
2816
|
-
})),
|
|
2817
|
-
}));
|
|
2818
|
-
},
|
|
2819
|
-
});
|
|
2820
|
-
this.selectSearchSubscriptions.set(normalizedKey, subscription);
|
|
2821
|
-
return search$;
|
|
2822
|
-
}
|
|
2823
|
-
cleanupSelectSearchStreams() {
|
|
2824
|
-
for (const subscription of this.selectSearchSubscriptions.values()) {
|
|
2825
|
-
subscription.unsubscribe();
|
|
2826
|
-
}
|
|
2827
|
-
this.selectSearchSubscriptions.clear();
|
|
2828
|
-
this.selectSearchChanges.clear();
|
|
2829
|
-
}
|
|
2830
3092
|
applyLookupResponse(field, response) {
|
|
2831
3093
|
const lookupMap = field.lookupMap || {};
|
|
2832
3094
|
const mapped = Object.entries(lookupMap).reduce((acc, [targetField, sourcePath]) => {
|
|
@@ -2837,7 +3099,6 @@ class UiDynamicFormComponent {
|
|
|
2837
3099
|
const updated = { ...current, ...mapped };
|
|
2838
3100
|
this.formContext.setValue(updated);
|
|
2839
3101
|
this.valueChange.emit(updated);
|
|
2840
|
-
this.ensureOptionsSourceOptionsLoaded(updated);
|
|
2841
3102
|
return updated;
|
|
2842
3103
|
});
|
|
2843
3104
|
}
|
|
@@ -2859,12 +3120,12 @@ class UiDynamicFormComponent {
|
|
|
2859
3120
|
getLeafFields() {
|
|
2860
3121
|
return this.getFlatFields().filter((field) => field.type !== 'container' && field.type !== 'template');
|
|
2861
3122
|
}
|
|
2862
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
2863
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDynamicFormComponent, isStandalone: true, selector: "forms-dynamic-form", inputs: { fields: "fields", value: "value", errors: "errors", submitLabel: "submitLabel", showSubmit: "showSubmit", disabled: "disabled" }, outputs: { valueChange: "valueChange", lookupCompleted: "lookupCompleted", submitted: "submitted", filesChange: "filesChange", uploadingChange: "uploadingChange", fieldActionTriggered: "fieldActionTriggered" }, providers: [UiFormContext, UiFormLookupService], usesOnChanges: true, ngImport: i0, template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n @if (isBarcodeField(field)) {\n <forms-barcode-scanner-input\n [label]=\"field.label || '\u0428\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [placeholder]=\"field.placeholder || '\u0412\u0456\u0434\u0441\u043A\u0430\u043D\u0443\u0439\u0442\u0435 \u0430\u0431\u043E \u0432\u0432\u0435\u0434\u0456\u0442\u044C \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [hint]=\"field.hint || '\u0421\u043A\u0430\u043D\u0435\u0440 \u043C\u043E\u0436\u0435 \u043F\u0440\u043E\u0441\u0442\u043E \u0432\u0432\u0435\u0441\u0442\u0438 \u043A\u043E\u0434 \u0456 \u043D\u0430\u0442\u0438\u0441\u043D\u0443\u0442\u0438 Enter.'\"\n [disabled]=\"isFieldDisabled(field) || isLookupLoading(field.id)\"\n [autoOpen]=\"field.autoOpen !== false\"\n [value]=\"getStringValue(field.id)\"\n [searchButtonLabel]=\"field.searchButtonLabel || '\u0417\u043D\u0430\u0439\u0442\u0438'\"\n [openCameraLabel]=\"field.openCameraLabel || '\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [closeCameraLabel]=\"field.closeCameraLabel || '\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [cameraTitle]=\"field.cameraTitle || '\u041A\u0430\u043C\u0435\u0440\u0430'\"\n [cameraStartingLabel]=\"field.cameraStartingLabel || '\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u0454\u043C\u043E \u043A\u0430\u043C\u0435\u0440\u0443\u2026'\"\n [cameraHint]=\"field.cameraHint || '\u041D\u0430\u0432\u0435\u0434\u0456\u0442\u044C \u043A\u0430\u043C\u0435\u0440\u0443 \u043D\u0430 \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434. \u041F\u0456\u0441\u043B\u044F \u0437\u0447\u0438\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043A\u043E\u0434 \u043F\u0456\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E.'\"\n (valueChange)=\"onBarcodeValueChange(field, $event)\"\n (searchRequested)=\"runBarcodeLookup(field)\"\n (scanned)=\"onBarcodeScanned(field, $event)\"\n ></forms-barcode-scanner-input>\n @if (getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (isInputField(field)) {\n <forms-form-input-host\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled\"\n [error]=\"errors[field.id] ?? ''\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"updateField(field.id, $any($event))\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n ></forms-form-input-host>\n }\n\n @if (field.type === 'select') {\n <forms-select\n [name]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [data]=\"field.data\"\n [required]=\"!!field.required\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [options]=\"resolveSelectOptions(field)\"\n [searchable]=\"(field.searchable ?? !!field.data) || !!field.optionsSource\"\n [remoteSearch]=\"!!field.optionsSource\"\n (searchChange)=\"field.optionsSource ? onSelectSearchChange(field, $event) : null\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"onSelectValueChange(field, $event)\">\n </forms-select>\n @if (field.lookupApi && getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (field.type === 'checkbox') {\n <forms-checkbox-input\n [label]=\"field.label\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"!!modelSignal()[field.id]\"\n (valueChange)=\"updateField(field.id, $event)\"\n ></forms-checkbox-input>\n }\n\n @if (field.type === 'color') {\n <forms-color-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-color-input>\n }\n\n @if (field.type === 'slug') {\n <forms-slug-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [readonly]=\"!!field.readonly\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-slug-input>\n }\n\n @if (field.type === 'photo') {\n <forms-photo-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept ?? 'image/*'\"\n [multiple]=\"!!field.multiple\"\n [limit]=\"field.limit ?? 0\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></forms-photo-input>\n }\n @if (field.type === 'file') {\n <forms-file-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n ></forms-file-input>\n }\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiFormInputHostComponent, selector: "forms-form-input-host", inputs: ["field", "value", "error", "disabled", "actions", "options"], outputs: ["valueChange", "actionTriggered"] }, { kind: "component", type: UiFileInputComponent, selector: "forms-file-input", inputs: ["label", "hint", "accept", "multiple", "disabled", "value"], outputs: ["valueChange", "filesChange"] }, { kind: "component", type: UiPhotoInputComponent, selector: "forms-photo-input", inputs: ["label", "hint", "accept", "multiple", "limit", "disabled", "value"], outputs: ["valueChange", "filesChange", "uploadingChange"] }, { kind: "component", type: UiCheckboxInputComponent, selector: "forms-checkbox-input", inputs: ["label", "disabled", "value"], outputs: ["valueChange"] }, { kind: "component", type: UiColorInputComponent, selector: "forms-color-input", inputs: ["label", "hint", "name", "id", "required", "fullWidth", "error"] }, { kind: "component", type: UiSlugInputComponent, selector: "forms-slug-input", inputs: ["label", "hint", "placeholder", "name", "id", "required", "fullWidth", "autocomplete", "readonly", "error"], outputs: ["invalidInput"] }, { kind: "component", type: UiBarcodeScannerInputComponent, selector: "forms-barcode-scanner-input", inputs: ["value", "label", "placeholder", "hint", "disabled", "autoOpen", "searchButtonLabel", "openCameraLabel", "closeCameraLabel", "cameraTitle", "cameraStartingLabel", "cameraHint"], outputs: ["valueChange", "scanned", "searchRequested", "cameraStateChange"] }, { kind: "component", type: UiSelectComponent, selector: "forms-select", inputs: ["label", "placeholder", "name", "data", "required", "fullWidth", "disabled", "multiple", "includeBlankOption", "blankOptionLabel", "emptyStateLabel", "searchable", "remoteSearch", "searchPlaceholder", "options", "error", "hint", "triggerClass", "panelClass"], outputs: ["valueChange", "searchChange"] }] });
|
|
3123
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3124
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: DynamicFormComponent, isStandalone: true, selector: "dynamic-form", inputs: { fields: "fields", value: "value", errors: "errors", submitLabel: "submitLabel", showSubmit: "showSubmit", disabled: "disabled" }, outputs: { valueChange: "valueChange", lookupCompleted: "lookupCompleted", submitted: "submitted", filesChange: "filesChange", uploadingChange: "uploadingChange", fieldActionTriggered: "fieldActionTriggered" }, providers: [UiFormContext, UiFormLookupService], usesOnChanges: true, ngImport: i0, template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n <field-input\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled || isFieldDisabled(field) || isLookupLoading(field.id)\"\n [error]=\"errors[field.id] ?? getLookupError(field.id)\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"onFieldValueChange(field, $event)\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n (lookupRequested)=\"runBarcodeLookup(field)\"\n (barcodeScanned)=\"onBarcodeScanned(field, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></field-input>\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: FieldInputComponent, selector: "field-input", inputs: ["field", "value", "error", "disabled", "actions"], outputs: ["valueChange", "actionTriggered", "lookupRequested", "barcodeScanned", "filesChange", "uploadingChange"] }] });
|
|
2864
3125
|
}
|
|
2865
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3126
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
2866
3127
|
type: Component,
|
|
2867
|
-
args: [{ selector: 'forms-dynamic-form', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, UiFormInputHostComponent, UiFileInputComponent, UiPhotoInputComponent, UiCheckboxInputComponent, UiColorInputComponent, UiSlugInputComponent, UiBarcodeScannerInputComponent, UiSelectComponent], providers: [UiFormContext, UiFormLookupService], template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n @if (isBarcodeField(field)) {\n <forms-barcode-scanner-input\n [label]=\"field.label || '\u0428\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [placeholder]=\"field.placeholder || '\u0412\u0456\u0434\u0441\u043A\u0430\u043D\u0443\u0439\u0442\u0435 \u0430\u0431\u043E \u0432\u0432\u0435\u0434\u0456\u0442\u044C \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [hint]=\"field.hint || '\u0421\u043A\u0430\u043D\u0435\u0440 \u043C\u043E\u0436\u0435 \u043F\u0440\u043E\u0441\u0442\u043E \u0432\u0432\u0435\u0441\u0442\u0438 \u043A\u043E\u0434 \u0456 \u043D\u0430\u0442\u0438\u0441\u043D\u0443\u0442\u0438 Enter.'\"\n [disabled]=\"isFieldDisabled(field) || isLookupLoading(field.id)\"\n [autoOpen]=\"field.autoOpen !== false\"\n [value]=\"getStringValue(field.id)\"\n [searchButtonLabel]=\"field.searchButtonLabel || '\u0417\u043D\u0430\u0439\u0442\u0438'\"\n [openCameraLabel]=\"field.openCameraLabel || '\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [closeCameraLabel]=\"field.closeCameraLabel || '\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [cameraTitle]=\"field.cameraTitle || '\u041A\u0430\u043C\u0435\u0440\u0430'\"\n [cameraStartingLabel]=\"field.cameraStartingLabel || '\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u0454\u043C\u043E \u043A\u0430\u043C\u0435\u0440\u0443\u2026'\"\n [cameraHint]=\"field.cameraHint || '\u041D\u0430\u0432\u0435\u0434\u0456\u0442\u044C \u043A\u0430\u043C\u0435\u0440\u0443 \u043D\u0430 \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434. \u041F\u0456\u0441\u043B\u044F \u0437\u0447\u0438\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043A\u043E\u0434 \u043F\u0456\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E.'\"\n (valueChange)=\"onBarcodeValueChange(field, $event)\"\n (searchRequested)=\"runBarcodeLookup(field)\"\n (scanned)=\"onBarcodeScanned(field, $event)\"\n ></forms-barcode-scanner-input>\n @if (getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (isInputField(field)) {\n <forms-form-input-host\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled\"\n [error]=\"errors[field.id] ?? ''\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"updateField(field.id, $any($event))\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n ></forms-form-input-host>\n }\n\n @if (field.type === 'select') {\n <forms-select\n [name]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [data]=\"field.data\"\n [required]=\"!!field.required\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [options]=\"resolveSelectOptions(field)\"\n [searchable]=\"(field.searchable ?? !!field.data) || !!field.optionsSource\"\n [remoteSearch]=\"!!field.optionsSource\"\n (searchChange)=\"field.optionsSource ? onSelectSearchChange(field, $event) : null\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"onSelectValueChange(field, $event)\">\n </forms-select>\n @if (field.lookupApi && getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (field.type === 'checkbox') {\n <forms-checkbox-input\n [label]=\"field.label\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"!!modelSignal()[field.id]\"\n (valueChange)=\"updateField(field.id, $event)\"\n ></forms-checkbox-input>\n }\n\n @if (field.type === 'color') {\n <forms-color-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-color-input>\n }\n\n @if (field.type === 'slug') {\n <forms-slug-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [readonly]=\"!!field.readonly\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-slug-input>\n }\n\n @if (field.type === 'photo') {\n <forms-photo-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept ?? 'image/*'\"\n [multiple]=\"!!field.multiple\"\n [limit]=\"field.limit ?? 0\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></forms-photo-input>\n }\n @if (field.type === 'file') {\n <forms-file-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n ></forms-file-input>\n }\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n" }]
|
|
3128
|
+
args: [{ selector: 'dynamic-form', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, FieldInputComponent], providers: [UiFormContext, UiFormLookupService], template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n <field-input\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled || isFieldDisabled(field) || isLookupLoading(field.id)\"\n [error]=\"errors[field.id] ?? getLookupError(field.id)\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"onFieldValueChange(field, $event)\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n (lookupRequested)=\"runBarcodeLookup(field)\"\n (barcodeScanned)=\"onBarcodeScanned(field, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></field-input>\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n" }]
|
|
2868
3129
|
}], propDecorators: { fields: [{
|
|
2869
3130
|
type: Input
|
|
2870
3131
|
}], value: [{
|
|
@@ -2891,7 +3152,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
2891
3152
|
type: Output
|
|
2892
3153
|
}] } });
|
|
2893
3154
|
|
|
2894
|
-
class
|
|
3155
|
+
class FormModalComponent {
|
|
2895
3156
|
formId = input('', ...(ngDevMode ? [{ debugName: "formId" }] : /* istanbul ignore next */ []));
|
|
2896
3157
|
recordId = input('', ...(ngDevMode ? [{ debugName: "recordId" }] : /* istanbul ignore next */ []));
|
|
2897
3158
|
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
@@ -2902,6 +3163,7 @@ class UiActionFormModalComponent {
|
|
|
2902
3163
|
completed = output();
|
|
2903
3164
|
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
2904
3165
|
isSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitting" }] : /* istanbul ignore next */ []));
|
|
3166
|
+
isActionRunning = signal(false, ...(ngDevMode ? [{ debugName: "isActionRunning" }] : /* istanbul ignore next */ []));
|
|
2905
3167
|
isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : /* istanbul ignore next */ []));
|
|
2906
3168
|
formConfig = signal(null, ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
|
|
2907
3169
|
formValue = signal({}, ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
@@ -2915,17 +3177,14 @@ class UiActionFormModalComponent {
|
|
|
2915
3177
|
}), ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
|
|
2916
3178
|
http = inject(HttpClient);
|
|
2917
3179
|
router = inject(Router);
|
|
3180
|
+
toast = inject(UiToastService);
|
|
2918
3181
|
ngOnChanges(changes) {
|
|
2919
3182
|
if (this.isOpen() && ('isOpen' in changes || 'formId' in changes || 'recordId' in changes)) {
|
|
2920
3183
|
this.loadForm();
|
|
2921
3184
|
}
|
|
2922
3185
|
}
|
|
2923
|
-
close() {
|
|
2924
|
-
|
|
2925
|
-
}
|
|
2926
|
-
onValueChange(value) {
|
|
2927
|
-
this.formValue.set(value);
|
|
2928
|
-
}
|
|
3186
|
+
close() { this.closed.emit(); }
|
|
3187
|
+
onValueChange(value) { this.formValue.set(value); }
|
|
2929
3188
|
onLookupCompleted(event) {
|
|
2930
3189
|
const redirectTo = String(this.formConfig()?.redirectTo || '').trim();
|
|
2931
3190
|
if (!redirectTo)
|
|
@@ -2937,24 +3196,30 @@ class UiActionFormModalComponent {
|
|
|
2937
3196
|
void this.router.navigateByUrl(target.startsWith('/') ? target : `/${target}`);
|
|
2938
3197
|
}
|
|
2939
3198
|
}
|
|
2940
|
-
onUploadingChange(uploading) {
|
|
2941
|
-
this.isUploading.set(uploading);
|
|
2942
|
-
}
|
|
3199
|
+
onUploadingChange(uploading) { this.isUploading.set(uploading); }
|
|
2943
3200
|
onFieldActionTriggered(event) {
|
|
3201
|
+
if (this.isActionRunning() || this.isSubmitting())
|
|
3202
|
+
return;
|
|
2944
3203
|
const payload = this.normalizePayload({ ...this.formValue(), ...(event.action.payload || {}) });
|
|
2945
3204
|
const endpoint = this.resolveTemplate(this.resolveEndpoint(event.action.api), payload);
|
|
2946
3205
|
if (!endpoint)
|
|
2947
3206
|
return;
|
|
3207
|
+
this.isActionRunning.set(true);
|
|
2948
3208
|
this.http.request(this.resolveMethod(event.action.method), endpoint, { body: payload }).subscribe({
|
|
2949
3209
|
next: (response) => {
|
|
2950
3210
|
const data = this.extractData(response);
|
|
2951
3211
|
if (data)
|
|
2952
3212
|
this.formValue.update((current) => ({ ...current, ...data }));
|
|
3213
|
+
this.isActionRunning.set(false);
|
|
3214
|
+
},
|
|
3215
|
+
error: (error) => {
|
|
3216
|
+
this.isActionRunning.set(false);
|
|
3217
|
+
this.toast.show(error?.error?.message || 'Не вдалося виконати дію', 'error', 4000);
|
|
2953
3218
|
},
|
|
2954
3219
|
});
|
|
2955
3220
|
}
|
|
2956
3221
|
confirm() {
|
|
2957
|
-
if (this.isSubmitting())
|
|
3222
|
+
if (this.isSubmitting() || this.isActionRunning() || this.isUploading())
|
|
2958
3223
|
return;
|
|
2959
3224
|
const action = this.submitAction();
|
|
2960
3225
|
const endpointTemplate = this.formConfig()?.api || action?.api;
|
|
@@ -2962,6 +3227,13 @@ class UiActionFormModalComponent {
|
|
|
2962
3227
|
if (!endpoint)
|
|
2963
3228
|
return;
|
|
2964
3229
|
const payload = this.normalizePayload({ ...(this.initialValue() || {}), ...(action?.payload || {}), ...this.formValue() });
|
|
3230
|
+
const missingFields = flattenDynamicFormFields(this.fields())
|
|
3231
|
+
.filter((field) => field.type !== 'container' && field.type !== 'template' && field.required && this.isEmptyValue(payload[field.id]))
|
|
3232
|
+
.map((field) => String(field.label || field.title || field.id));
|
|
3233
|
+
if (missingFields.length) {
|
|
3234
|
+
this.toast.show(`Заповніть обов’язкові поля: ${missingFields.join(', ')}`, 'warning', 4000);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
2965
3237
|
const recordId = String(this.recordId() || '').trim();
|
|
2966
3238
|
if (recordId && !Object.prototype.hasOwnProperty.call(payload, 'id'))
|
|
2967
3239
|
payload['id'] = recordId;
|
|
@@ -2976,7 +3248,10 @@ class UiActionFormModalComponent {
|
|
|
2976
3248
|
this.navigateAfterSubmit(response);
|
|
2977
3249
|
this.close();
|
|
2978
3250
|
},
|
|
2979
|
-
error: () =>
|
|
3251
|
+
error: (error) => {
|
|
3252
|
+
this.isSubmitting.set(false);
|
|
3253
|
+
this.toast.show(error?.error?.message || 'Не вдалося відправити форму', 'error', 4000);
|
|
3254
|
+
},
|
|
2980
3255
|
});
|
|
2981
3256
|
}
|
|
2982
3257
|
loadForm() {
|
|
@@ -2992,10 +3267,7 @@ class UiActionFormModalComponent {
|
|
|
2992
3267
|
const config = {
|
|
2993
3268
|
...raw,
|
|
2994
3269
|
title: raw.title || raw.meta?.title,
|
|
2995
|
-
actions: (raw.actions || []).map((action) => ({
|
|
2996
|
-
...action,
|
|
2997
|
-
type: this.normalizeActionType(action.type),
|
|
2998
|
-
})),
|
|
3270
|
+
actions: (raw.actions || []).map((action) => ({ ...action, type: this.normalizeActionType(action.type) })),
|
|
2999
3271
|
};
|
|
3000
3272
|
this.formConfig.set(config);
|
|
3001
3273
|
this.formValue.set({ ...(config.defaults || {}), ...(config.item || {}), ...(this.initialValue() || {}), ...(recordId ? { id: recordId } : {}) });
|
|
@@ -3020,12 +3292,22 @@ class UiActionFormModalComponent {
|
|
|
3020
3292
|
if (field.type !== 'photo' || !field.multiple)
|
|
3021
3293
|
continue;
|
|
3022
3294
|
const value = payload[field.id];
|
|
3023
|
-
if (typeof value === 'string')
|
|
3295
|
+
if (typeof value === 'string')
|
|
3024
3296
|
payload[field.id] = value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
3025
|
-
}
|
|
3026
3297
|
}
|
|
3027
3298
|
return payload;
|
|
3028
3299
|
}
|
|
3300
|
+
isEmptyValue(value) {
|
|
3301
|
+
if (value == null)
|
|
3302
|
+
return true;
|
|
3303
|
+
if (Array.isArray(value))
|
|
3304
|
+
return value.length === 0;
|
|
3305
|
+
if (typeof value === 'string')
|
|
3306
|
+
return value.trim() === '';
|
|
3307
|
+
if (typeof value === 'number')
|
|
3308
|
+
return !Number.isFinite(value);
|
|
3309
|
+
return false;
|
|
3310
|
+
}
|
|
3029
3311
|
normalizeActionType(type) {
|
|
3030
3312
|
const normalized = String(type || '').trim().toLowerCase();
|
|
3031
3313
|
if (normalized === 'api' || normalized === 'action_api')
|
|
@@ -3063,15 +3345,15 @@ class UiActionFormModalComponent {
|
|
|
3063
3345
|
return value == null || value === '' ? '' : encodeURIComponent(String(value));
|
|
3064
3346
|
});
|
|
3065
3347
|
}
|
|
3066
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3067
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
3348
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3349
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormModalComponent, isStandalone: true, selector: "form-modal", inputs: { formId: { classPropertyName: "formId", publicName: "formId", isSignal: true, isRequired: false, transformFunction: null }, recordId: { classPropertyName: "recordId", publicName: "recordId", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, cancelLabel: { classPropertyName: "cancelLabel", publicName: "cancelLabel", isSignal: true, isRequired: false, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", completed: "completed" }, usesOnChanges: true, ngImport: i0, template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [disabled]=\"isLoading() || isSubmitting() || isActionRunning()\" [showSubmit]=\"false\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: DynamicFormComponent, selector: "dynamic-form", inputs: ["fields", "value", "errors", "submitLabel", "showSubmit", "disabled"], outputs: ["valueChange", "lookupCompleted", "submitted", "filesChange", "uploadingChange", "fieldActionTriggered"] }] });
|
|
3068
3350
|
}
|
|
3069
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3351
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormModalComponent, decorators: [{
|
|
3070
3352
|
type: Component,
|
|
3071
|
-
args: [{ selector: '
|
|
3353
|
+
args: [{ selector: 'form-modal', standalone: true, imports: [CommonModule, UiButtonComponent, DynamicFormComponent], template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex min-h-full items-center justify-center overflow-y-auto bg-slate-950/40 p-4 sm:p-6\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"my-0 flex min-h-0 max-h-[calc(100vh-2rem)] w-full flex-col overflow-hidden rounded-2xl bg-slate-50 shadow-2xl sm:max-h-[calc(100vh-3rem)]\" [ngClass]=\"panelClass()\">\n <div class=\"flex shrink-0 items-center justify-between border-b border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"min-h-0 flex-1 overflow-y-auto p-4 sm:p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [disabled]=\"isLoading() || isSubmitting() || isActionRunning()\" [showSubmit]=\"false\" (valueChange)=\"onValueChange($event)\" (lookupCompleted)=\"onLookupCompleted($event)\" (uploadingChange)=\"onUploadingChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></dynamic-form>\n }\n </div>\n <div class=\"relative z-10 flex shrink-0 justify-end gap-3 border-t border-slate-200 bg-white px-5 py-4 sm:px-6\">\n <ui-button variant=\"ghost\" (click)=\"close()\">{{ cancelLabel() }}</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting() || isUploading()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n" }]
|
|
3072
3354
|
}], propDecorators: { formId: [{ type: i0.Input, args: [{ isSignal: true, alias: "formId", required: false }] }], recordId: [{ type: i0.Input, args: [{ isSignal: true, alias: "recordId", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], cancelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelLabel", required: false }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], completed: [{ type: i0.Output, args: ["completed"] }] } });
|
|
3073
3355
|
|
|
3074
|
-
class
|
|
3356
|
+
class InputComponent {
|
|
3075
3357
|
cdr = inject(ChangeDetectorRef);
|
|
3076
3358
|
label;
|
|
3077
3359
|
hint;
|
|
@@ -3084,14 +3366,13 @@ class UiInputComponent {
|
|
|
3084
3366
|
required = false;
|
|
3085
3367
|
fullWidth = false;
|
|
3086
3368
|
autocomplete;
|
|
3087
|
-
readonly = false;
|
|
3088
3369
|
pattern;
|
|
3089
3370
|
error;
|
|
3090
3371
|
actions = [];
|
|
3091
3372
|
disabled = false;
|
|
3092
3373
|
value = '';
|
|
3093
3374
|
static nextId = 0;
|
|
3094
|
-
generatedId = `
|
|
3375
|
+
generatedId = `input-${InputComponent.nextId++}`;
|
|
3095
3376
|
actionTriggered = new EventEmitter();
|
|
3096
3377
|
get controlId() {
|
|
3097
3378
|
return this.id ?? this.generatedId;
|
|
@@ -3120,24 +3401,24 @@ class UiInputComponent {
|
|
|
3120
3401
|
triggerAction(action) {
|
|
3121
3402
|
this.actionTriggered.emit(action);
|
|
3122
3403
|
}
|
|
3123
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3124
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
3404
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3405
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputComponent, isStandalone: true, selector: "input-base", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", type: "type", element: "element", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
3125
3406
|
{
|
|
3126
3407
|
provide: NG_VALUE_ACCESSOR,
|
|
3127
|
-
useExisting: forwardRef(() =>
|
|
3408
|
+
useExisting: forwardRef(() => InputComponent),
|
|
3128
3409
|
multi: true,
|
|
3129
3410
|
},
|
|
3130
|
-
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [
|
|
3411
|
+
], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3131
3412
|
}
|
|
3132
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3413
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputComponent, decorators: [{
|
|
3133
3414
|
type: Component,
|
|
3134
|
-
args: [{ selector: '
|
|
3415
|
+
args: [{ selector: 'input-base', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
3135
3416
|
{
|
|
3136
3417
|
provide: NG_VALUE_ACCESSOR,
|
|
3137
|
-
useExisting: forwardRef(() =>
|
|
3418
|
+
useExisting: forwardRef(() => InputComponent),
|
|
3138
3419
|
multi: true,
|
|
3139
3420
|
},
|
|
3140
|
-
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [
|
|
3421
|
+
], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n" }]
|
|
3141
3422
|
}], propDecorators: { label: [{
|
|
3142
3423
|
type: Input
|
|
3143
3424
|
}], hint: [{
|
|
@@ -3160,8 +3441,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3160
3441
|
type: Input
|
|
3161
3442
|
}], autocomplete: [{
|
|
3162
3443
|
type: Input
|
|
3163
|
-
}], readonly: [{
|
|
3164
|
-
type: Input
|
|
3165
3444
|
}], pattern: [{
|
|
3166
3445
|
type: Input
|
|
3167
3446
|
}], error: [{
|
|
@@ -3172,7 +3451,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3172
3451
|
type: Output
|
|
3173
3452
|
}] } });
|
|
3174
3453
|
|
|
3175
|
-
class
|
|
3454
|
+
class InputPhotoGalleryComponent {
|
|
3176
3455
|
http = inject(HttpClient);
|
|
3177
3456
|
label = 'Фото';
|
|
3178
3457
|
orderLabel = 'Порядок фото (перетягуй)';
|
|
@@ -3314,12 +3593,12 @@ class UiPhotoGalleryFieldComponent {
|
|
|
3314
3593
|
formData.append('file', file);
|
|
3315
3594
|
return this.http.post('/api/files/', formData).pipe(map((response) => ({ imageId: response.id })));
|
|
3316
3595
|
}
|
|
3317
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3318
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type:
|
|
3596
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoGalleryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3597
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InputPhotoGalleryComponent, isStandalone: true, selector: "input-photo-gallery", inputs: { label: "label", orderLabel: "orderLabel", imageIds: "imageIds", disabled: "disabled", afterUpload: "afterUpload", resolveImageUrl: "resolveImageUrl", notify: "notify" }, outputs: { imageIdsChange: "imageIdsChange" }, usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
|
|
3319
3598
|
}
|
|
3320
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3599
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputPhotoGalleryComponent, decorators: [{
|
|
3321
3600
|
type: Component,
|
|
3322
|
-
args: [{ selector: '
|
|
3601
|
+
args: [{ selector: 'input-photo-gallery', standalone: true, imports: [CommonModule, DragDropModule], template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"] }]
|
|
3323
3602
|
}], propDecorators: { label: [{
|
|
3324
3603
|
type: Input
|
|
3325
3604
|
}], orderLabel: [{
|
|
@@ -3338,7 +3617,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3338
3617
|
type: Output
|
|
3339
3618
|
}] } });
|
|
3340
3619
|
|
|
3341
|
-
class
|
|
3620
|
+
class InputRangeSliderComponent {
|
|
3342
3621
|
min = 0;
|
|
3343
3622
|
max = 100;
|
|
3344
3623
|
step = 1;
|
|
@@ -3427,8 +3706,8 @@ class UiRangeSliderComponent {
|
|
|
3427
3706
|
input.step = String(this.safeStep);
|
|
3428
3707
|
input.value = String(value);
|
|
3429
3708
|
}
|
|
3430
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3431
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type:
|
|
3709
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputRangeSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3710
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: InputRangeSliderComponent, isStandalone: true, selector: "input-range-slider", inputs: { min: "min", max: "max", step: "step", startValue: "startValue", endValue: "endValue", startLabel: "startLabel", endLabel: "endLabel" }, outputs: { startValueChange: "startValueChange", endValueChange: "endValueChange", interactionEnd: "interactionEnd" }, viewQueries: [{ propertyName: "startInput", first: true, predicate: ["startInput"], descendants: true }, { propertyName: "endInput", first: true, predicate: ["endInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
3432
3711
|
<div class="range-slider">
|
|
3433
3712
|
<div class="range-slider__track"></div>
|
|
3434
3713
|
<div
|
|
@@ -3463,9 +3742,9 @@ class UiRangeSliderComponent {
|
|
|
3463
3742
|
</div>
|
|
3464
3743
|
`, isInline: true, styles: [".range-slider{position:relative;height:2rem;width:100%;display:flex;align-items:center}.range-slider__track,.range-slider__range{position:absolute;left:0;right:0;height:6px;border-radius:999px}.range-slider__track{background:#dbe1ea}.range-slider__range{background:linear-gradient(90deg,#334155,#0f172a)}.range-slider__input{pointer-events:none;position:absolute;left:0;width:100%;height:2rem;margin:0;background:transparent;appearance:none;-webkit-appearance:none}.range-slider__input::-webkit-slider-runnable-track{height:6px;background:transparent}.range-slider__input::-moz-range-track{height:6px;background:transparent}.range-slider__input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;pointer-events:auto;width:20px;height:20px;margin-top:-7px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}.range-slider__input::-moz-range-thumb{pointer-events:auto;width:20px;height:20px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
3465
3744
|
}
|
|
3466
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3745
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputRangeSliderComponent, decorators: [{
|
|
3467
3746
|
type: Component,
|
|
3468
|
-
args: [{ selector: '
|
|
3747
|
+
args: [{ selector: 'input-range-slider', standalone: true, imports: [CommonModule], template: `
|
|
3469
3748
|
<div class="range-slider">
|
|
3470
3749
|
<div class="range-slider__track"></div>
|
|
3471
3750
|
<div
|
|
@@ -3531,5 +3810,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3531
3810
|
* Generated bundle index. Do not edit.
|
|
3532
3811
|
*/
|
|
3533
3812
|
|
|
3534
|
-
export { ACTION_API, ACTION_FORM, ACTION_PRINT, ACTION_SUBMIT,
|
|
3813
|
+
export { ACTION_API, ACTION_FORM, ACTION_PRINT, ACTION_SUBMIT, DynamicFormComponent, FieldInputComponent, FormModalComponent, InputBarcodeComponent, InputCheckboxComponent, InputColorComponent, InputComponent, InputDateComponent, InputDatetimeComponent, InputEmailComponent, InputFileComponent, InputNumberComponent, InputPasswordComponent, InputPhotoComponent, InputPhotoGalleryComponent, InputRangeSliderComponent, InputSelectComponent, InputSlugComponent, InputTextComponent, InputTextareaComponent, MaroFormsApiService, UI_FORM_INPUT_REGISTRY, UiFormContext, UiFormLookupService, flattenDynamicFormFields, isDynamicFormContainerField, isDynamicFormTemplateField };
|
|
3535
3814
|
//# sourceMappingURL=maro-tech-form.mjs.map
|