@maro-tech/form 0.0.17 → 0.1.1
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 +1485 -1228
- package/fesm2022/maro-tech-form.mjs.map +1 -1
- package/package.json +2 -2
- package/types/maro-tech-form.d.ts +103 -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, UiToastService } 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,248 +1029,210 @@ 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();
|
|
1101
|
+
else
|
|
1102
|
+
this.value = masked;
|
|
1109
1103
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
this.
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1104
|
+
else
|
|
1105
|
+
this.value = masked; this.onChange(this.value); this.cdr.markForCheck(); }
|
|
1106
|
+
allowDateKey(event) { if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
1107
|
+
return; if (!/^\d$/.test(event.key))
|
|
1108
|
+
event.preventDefault(); }
|
|
1109
|
+
applyMask(value) { const digits = value.replace(/\D/g, '').slice(0, 12); const date = [digits.slice(0, 2), digits.slice(2, 4), digits.slice(4, 8)].filter(Boolean).join('.'); const time = [digits.slice(8, 10), digits.slice(10, 12)].filter(Boolean).join(':'); return [date, time].filter(Boolean).join(' '); }
|
|
1110
|
+
isSelected(date) { return this.selectedDate ? this.toDateValue(this.selectedDate) === date : false; }
|
|
1111
|
+
isSelectedMonth(month) { return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear(); }
|
|
1112
|
+
isSelectedYear(year) { return this.selectedDate?.getFullYear() === year; }
|
|
1113
|
+
onDocumentClick() { if (this.isCalendarOpen)
|
|
1114
|
+
this.closeCalendar(); }
|
|
1115
|
+
onEscape() { if (this.isCalendarOpen)
|
|
1116
|
+
this.closeCalendar(); }
|
|
1117
|
+
triggerAction(action) { this.actionTriggered.emit(action); }
|
|
1118
|
+
startOfMonth(date) { return new Date(date.getFullYear(), date.getMonth(), 1); }
|
|
1119
|
+
parseDate(value) { const [year, month, day] = value.split('-').map(Number); return year && month && day ? new Date(year, month - 1, day) : null; }
|
|
1120
|
+
toDateValue(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; }
|
|
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 });
|
|
1126
|
+
}
|
|
1127
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InputDatetimeComponent, decorators: [{
|
|
1128
|
+
type: Component,
|
|
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" }]
|
|
1133
|
+
}], propDecorators: { label: [{
|
|
1134
|
+
type: Input
|
|
1135
|
+
}], hint: [{
|
|
1136
|
+
type: Input
|
|
1137
|
+
}], placeholder: [{
|
|
1138
|
+
type: Input
|
|
1139
|
+
}], name: [{
|
|
1140
|
+
type: Input
|
|
1141
|
+
}], id: [{
|
|
1142
|
+
type: Input
|
|
1143
|
+
}], rows: [{
|
|
1144
|
+
type: Input
|
|
1145
|
+
}], required: [{
|
|
1146
|
+
type: Input
|
|
1147
|
+
}], fullWidth: [{
|
|
1148
|
+
type: Input
|
|
1149
|
+
}], autocomplete: [{
|
|
1150
|
+
type: Input
|
|
1151
|
+
}], pattern: [{
|
|
1152
|
+
type: Input
|
|
1153
|
+
}], error: [{
|
|
1154
|
+
type: Input
|
|
1155
|
+
}], actions: [{
|
|
1156
|
+
type: Input
|
|
1157
|
+
}], actionTriggered: [{
|
|
1158
|
+
type: Output
|
|
1159
|
+
}], onDocumentClick: [{
|
|
1160
|
+
type: HostListener,
|
|
1161
|
+
args: ['document:click']
|
|
1162
|
+
}], onEscape: [{
|
|
1163
|
+
type: HostListener,
|
|
1164
|
+
args: ['document:keydown.escape']
|
|
1165
|
+
}] } });
|
|
1166
|
+
|
|
1167
|
+
class InputEmailComponent {
|
|
1168
|
+
cdr = inject(ChangeDetectorRef);
|
|
1169
|
+
label;
|
|
1170
|
+
hint;
|
|
1171
|
+
placeholder;
|
|
1172
|
+
name;
|
|
1173
|
+
id;
|
|
1174
|
+
type = 'email';
|
|
1175
|
+
element = 'input';
|
|
1176
|
+
rows = 4;
|
|
1177
|
+
required = false;
|
|
1178
|
+
fullWidth = false;
|
|
1179
|
+
autocomplete;
|
|
1180
|
+
pattern;
|
|
1181
|
+
error;
|
|
1182
|
+
actions = [];
|
|
1183
|
+
disabled = false;
|
|
1184
|
+
value = '';
|
|
1185
|
+
static nextId = 0;
|
|
1186
|
+
generatedId = `input-email-${InputEmailComponent.nextId++}`;
|
|
1187
|
+
actionTriggered = new EventEmitter();
|
|
1188
|
+
get controlId() {
|
|
1189
|
+
return this.id ?? this.generatedId;
|
|
1144
1190
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
this.value =
|
|
1149
|
-
this.onChange(dateValue);
|
|
1150
|
-
this.closeCalendar();
|
|
1151
|
-
this.onTouched();
|
|
1191
|
+
onChange = () => undefined;
|
|
1192
|
+
onTouched = () => undefined;
|
|
1193
|
+
writeValue(value) {
|
|
1194
|
+
this.value = value ?? '';
|
|
1152
1195
|
this.cdr.markForCheck();
|
|
1153
1196
|
}
|
|
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
1197
|
registerOnChange(fn) {
|
|
1174
1198
|
this.onChange = fn;
|
|
1175
1199
|
}
|
|
1176
1200
|
registerOnTouched(fn) {
|
|
1177
1201
|
this.onTouched = fn;
|
|
1178
1202
|
}
|
|
1203
|
+
validate(control) { return validateFormControl(control, this.required, this.pattern, true); }
|
|
1179
1204
|
setDisabledState(isDisabled) {
|
|
1180
1205
|
this.disabled = isDisabled;
|
|
1181
1206
|
this.cdr.markForCheck();
|
|
1182
1207
|
}
|
|
1183
1208
|
onValueChange(value) {
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
this.
|
|
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('.');
|
|
1209
|
+
const parsed = value;
|
|
1210
|
+
this.value = parsed;
|
|
1211
|
+
this.onChange(parsed);
|
|
1205
1212
|
}
|
|
1206
1213
|
triggerAction(action) {
|
|
1207
1214
|
this.actionTriggered.emit(action);
|
|
1208
1215
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
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: [
|
|
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: [
|
|
1246
1218
|
{
|
|
1247
1219
|
provide: NG_VALUE_ACCESSOR,
|
|
1248
|
-
useExisting: forwardRef(() =>
|
|
1220
|
+
useExisting: forwardRef(() => InputEmailComponent),
|
|
1249
1221
|
multi: true,
|
|
1250
1222
|
},
|
|
1251
|
-
|
|
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 });
|
|
1252
1225
|
}
|
|
1253
|
-
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: [{
|
|
1254
1227
|
type: Component,
|
|
1255
|
-
args: [{ selector: '
|
|
1228
|
+
args: [{ selector: 'input-email', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1256
1229
|
{
|
|
1257
1230
|
provide: NG_VALUE_ACCESSOR,
|
|
1258
|
-
useExisting: forwardRef(() =>
|
|
1231
|
+
useExisting: forwardRef(() => InputEmailComponent),
|
|
1259
1232
|
multi: true,
|
|
1260
1233
|
},
|
|
1261
|
-
|
|
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" }]
|
|
1262
1236
|
}], propDecorators: { label: [{
|
|
1263
1237
|
type: Input
|
|
1264
1238
|
}], hint: [{
|
|
@@ -1277,8 +1251,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1277
1251
|
type: Input
|
|
1278
1252
|
}], autocomplete: [{
|
|
1279
1253
|
type: Input
|
|
1280
|
-
}], readonly: [{
|
|
1281
|
-
type: Input
|
|
1282
1254
|
}], pattern: [{
|
|
1283
1255
|
type: Input
|
|
1284
1256
|
}], error: [{
|
|
@@ -1287,281 +1259,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
1287
1259
|
type: Input
|
|
1288
1260
|
}], actionTriggered: [{
|
|
1289
1261
|
type: Output
|
|
1290
|
-
}], onDocumentClick: [{
|
|
1291
|
-
type: HostListener,
|
|
1292
|
-
args: ['document:click']
|
|
1293
|
-
}], onEscape: [{
|
|
1294
|
-
type: HostListener,
|
|
1295
|
-
args: ['document:keydown.escape']
|
|
1296
1262
|
}] } });
|
|
1297
1263
|
|
|
1298
|
-
class
|
|
1264
|
+
class InputNumberComponent {
|
|
1299
1265
|
cdr = inject(ChangeDetectorRef);
|
|
1300
|
-
popupService = inject(UiPopupService);
|
|
1301
|
-
closeActivePopup = () => this.closeCalendar();
|
|
1302
1266
|
label;
|
|
1303
1267
|
hint;
|
|
1304
1268
|
placeholder;
|
|
1305
1269
|
name;
|
|
1306
1270
|
id;
|
|
1271
|
+
type = 'number';
|
|
1272
|
+
element = 'input';
|
|
1307
1273
|
rows = 4;
|
|
1308
1274
|
required = false;
|
|
1309
1275
|
fullWidth = false;
|
|
1310
|
-
autocomplete
|
|
1311
|
-
|
|
1276
|
+
autocomplete;
|
|
1277
|
+
disabled = false;
|
|
1312
1278
|
pattern;
|
|
1313
1279
|
error;
|
|
1314
1280
|
actions = [];
|
|
1315
|
-
type = 'datetime-local';
|
|
1316
|
-
calendarIcon = CalendarDays;
|
|
1317
|
-
previousMonthIcon = ChevronLeft;
|
|
1318
|
-
nextMonthIcon = ChevronRight;
|
|
1319
|
-
disabled = false;
|
|
1320
1281
|
value = '';
|
|
1321
|
-
isCalendarOpen = false;
|
|
1322
|
-
calendarView = 'days';
|
|
1323
|
-
visibleMonth = this.startOfMonth(new Date());
|
|
1324
|
-
selectedHour = 12;
|
|
1325
|
-
selectedMinute = 0;
|
|
1326
1282
|
static nextId = 0;
|
|
1327
|
-
generatedId = `
|
|
1283
|
+
generatedId = `input-number-${InputNumberComponent.nextId++}`;
|
|
1328
1284
|
actionTriggered = new EventEmitter();
|
|
1285
|
+
get controlId() {
|
|
1286
|
+
return this.id ?? this.generatedId;
|
|
1287
|
+
}
|
|
1329
1288
|
onChange = () => undefined;
|
|
1330
1289
|
onTouched = () => undefined;
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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;
|
|
1378
|
-
}
|
|
1379
|
-
else
|
|
1380
|
-
this.value = masked; this.onChange(this.value); this.cdr.markForCheck(); }
|
|
1381
|
-
allowDateKey(event) { if (event.ctrlKey || event.metaKey || ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
1382
|
-
return; if (!/^\d$/.test(event.key))
|
|
1383
|
-
event.preventDefault(); }
|
|
1384
|
-
applyMask(value) { const digits = value.replace(/\D/g, '').slice(0, 12); const date = [digits.slice(0, 2), digits.slice(2, 4), digits.slice(4, 8)].filter(Boolean).join('.'); const time = [digits.slice(8, 10), digits.slice(10, 12)].filter(Boolean).join(':'); return [date, time].filter(Boolean).join(' '); }
|
|
1385
|
-
isSelected(date) { return this.selectedDate ? this.toDateValue(this.selectedDate) === date : false; }
|
|
1386
|
-
isSelectedMonth(month) { return this.selectedDate?.getMonth() === month && this.selectedDate?.getFullYear() === this.visibleMonth.getFullYear(); }
|
|
1387
|
-
isSelectedYear(year) { return this.selectedDate?.getFullYear() === year; }
|
|
1388
|
-
onDocumentClick() { if (this.isCalendarOpen)
|
|
1389
|
-
this.closeCalendar(); }
|
|
1390
|
-
onEscape() { if (this.isCalendarOpen)
|
|
1391
|
-
this.closeCalendar(); }
|
|
1392
|
-
triggerAction(action) { this.actionTriggered.emit(action); }
|
|
1393
|
-
startOfMonth(date) { return new Date(date.getFullYear(), date.getMonth(), 1); }
|
|
1394
|
-
parseDate(value) { const [year, month, day] = value.split('-').map(Number); return year && month && day ? new Date(year, month - 1, day) : null; }
|
|
1395
|
-
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: UiDatetimeInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1397
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDatetimeInputComponent, isStandalone: true, selector: "forms-datetime-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: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiDatetimeInputComponent), multi: true }], 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\" [readonly]=\"readonly\" [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
|
-
}
|
|
1399
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDatetimeInputComponent, decorators: [{
|
|
1400
|
-
type: Component,
|
|
1401
|
-
args: [{ selector: 'forms-datetime-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, LucideAngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiDatetimeInputComponent), multi: true }], 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\" [readonly]=\"readonly\" [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
|
-
}], propDecorators: { label: [{
|
|
1403
|
-
type: Input
|
|
1404
|
-
}], hint: [{
|
|
1405
|
-
type: Input
|
|
1406
|
-
}], placeholder: [{
|
|
1407
|
-
type: Input
|
|
1408
|
-
}], name: [{
|
|
1409
|
-
type: Input
|
|
1410
|
-
}], id: [{
|
|
1411
|
-
type: Input
|
|
1412
|
-
}], rows: [{
|
|
1413
|
-
type: Input
|
|
1414
|
-
}], required: [{
|
|
1415
|
-
type: Input
|
|
1416
|
-
}], fullWidth: [{
|
|
1417
|
-
type: Input
|
|
1418
|
-
}], autocomplete: [{
|
|
1419
|
-
type: Input
|
|
1420
|
-
}], readonly: [{
|
|
1421
|
-
type: Input
|
|
1422
|
-
}], pattern: [{
|
|
1423
|
-
type: Input
|
|
1424
|
-
}], error: [{
|
|
1425
|
-
type: Input
|
|
1426
|
-
}], actions: [{
|
|
1427
|
-
type: Input
|
|
1428
|
-
}], actionTriggered: [{
|
|
1429
|
-
type: Output
|
|
1430
|
-
}], onDocumentClick: [{
|
|
1431
|
-
type: HostListener,
|
|
1432
|
-
args: ['document:click']
|
|
1433
|
-
}], onEscape: [{
|
|
1434
|
-
type: HostListener,
|
|
1435
|
-
args: ['document:keydown.escape']
|
|
1436
|
-
}] } });
|
|
1437
|
-
|
|
1438
|
-
class UiEmailInputComponent {
|
|
1439
|
-
cdr = inject(ChangeDetectorRef);
|
|
1440
|
-
label;
|
|
1441
|
-
hint;
|
|
1442
|
-
placeholder;
|
|
1443
|
-
name;
|
|
1444
|
-
id;
|
|
1445
|
-
type = 'email';
|
|
1446
|
-
element = 'input';
|
|
1447
|
-
rows = 4;
|
|
1448
|
-
required = false;
|
|
1449
|
-
fullWidth = false;
|
|
1450
|
-
autocomplete;
|
|
1451
|
-
readonly = false;
|
|
1452
|
-
pattern;
|
|
1453
|
-
error;
|
|
1454
|
-
actions = [];
|
|
1455
|
-
disabled = false;
|
|
1456
|
-
value = '';
|
|
1457
|
-
static nextId = 0;
|
|
1458
|
-
generatedId = `ui-input-${UiEmailInputComponent.nextId++}`;
|
|
1459
|
-
actionTriggered = new EventEmitter();
|
|
1460
|
-
get controlId() {
|
|
1461
|
-
return this.id ?? this.generatedId;
|
|
1462
|
-
}
|
|
1463
|
-
onChange = () => undefined;
|
|
1464
|
-
onTouched = () => undefined;
|
|
1465
|
-
writeValue(value) {
|
|
1466
|
-
this.value = value ?? '';
|
|
1467
|
-
this.cdr.markForCheck();
|
|
1468
|
-
}
|
|
1469
|
-
registerOnChange(fn) {
|
|
1470
|
-
this.onChange = fn;
|
|
1471
|
-
}
|
|
1472
|
-
registerOnTouched(fn) {
|
|
1473
|
-
this.onTouched = fn;
|
|
1474
|
-
}
|
|
1475
|
-
setDisabledState(isDisabled) {
|
|
1476
|
-
this.disabled = isDisabled;
|
|
1477
|
-
this.cdr.markForCheck();
|
|
1478
|
-
}
|
|
1479
|
-
onValueChange(value) {
|
|
1480
|
-
const parsed = value;
|
|
1481
|
-
this.value = parsed;
|
|
1482
|
-
this.onChange(parsed);
|
|
1483
|
-
}
|
|
1484
|
-
triggerAction(action) {
|
|
1485
|
-
this.actionTriggered.emit(action);
|
|
1486
|
-
}
|
|
1487
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1488
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiEmailInputComponent, isStandalone: true, selector: "forms-email-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" }, providers: [
|
|
1489
|
-
{
|
|
1490
|
-
provide: NG_VALUE_ACCESSOR,
|
|
1491
|
-
useExisting: forwardRef(() => UiEmailInputComponent),
|
|
1492
|
-
multi: true,
|
|
1493
|
-
},
|
|
1494
|
-
], 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 [readonly]=\"readonly\"\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\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
|
-
}
|
|
1496
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, decorators: [{
|
|
1497
|
-
type: Component,
|
|
1498
|
-
args: [{ selector: 'forms-email-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1499
|
-
{
|
|
1500
|
-
provide: NG_VALUE_ACCESSOR,
|
|
1501
|
-
useExisting: forwardRef(() => UiEmailInputComponent),
|
|
1502
|
-
multi: true,
|
|
1503
|
-
},
|
|
1504
|
-
], 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 [readonly]=\"readonly\"\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\n" }]
|
|
1505
|
-
}], propDecorators: { label: [{
|
|
1506
|
-
type: Input
|
|
1507
|
-
}], hint: [{
|
|
1508
|
-
type: Input
|
|
1509
|
-
}], placeholder: [{
|
|
1510
|
-
type: Input
|
|
1511
|
-
}], name: [{
|
|
1512
|
-
type: Input
|
|
1513
|
-
}], id: [{
|
|
1514
|
-
type: Input
|
|
1515
|
-
}], rows: [{
|
|
1516
|
-
type: Input
|
|
1517
|
-
}], required: [{
|
|
1518
|
-
type: Input
|
|
1519
|
-
}], fullWidth: [{
|
|
1520
|
-
type: Input
|
|
1521
|
-
}], autocomplete: [{
|
|
1522
|
-
type: Input
|
|
1523
|
-
}], readonly: [{
|
|
1524
|
-
type: Input
|
|
1525
|
-
}], pattern: [{
|
|
1526
|
-
type: Input
|
|
1527
|
-
}], error: [{
|
|
1528
|
-
type: Input
|
|
1529
|
-
}], actions: [{
|
|
1530
|
-
type: Input
|
|
1531
|
-
}], actionTriggered: [{
|
|
1532
|
-
type: Output
|
|
1533
|
-
}] } });
|
|
1534
|
-
|
|
1535
|
-
class UiNumberInputComponent {
|
|
1536
|
-
cdr = inject(ChangeDetectorRef);
|
|
1537
|
-
label;
|
|
1538
|
-
hint;
|
|
1539
|
-
placeholder;
|
|
1540
|
-
name;
|
|
1541
|
-
id;
|
|
1542
|
-
type = 'number';
|
|
1543
|
-
element = 'input';
|
|
1544
|
-
rows = 4;
|
|
1545
|
-
required = false;
|
|
1546
|
-
fullWidth = false;
|
|
1547
|
-
autocomplete;
|
|
1548
|
-
readonly = false;
|
|
1549
|
-
disabled = false;
|
|
1550
|
-
pattern;
|
|
1551
|
-
error;
|
|
1552
|
-
actions = [];
|
|
1553
|
-
value = '';
|
|
1554
|
-
static nextId = 0;
|
|
1555
|
-
generatedId = `ui-input-${UiNumberInputComponent.nextId++}`;
|
|
1556
|
-
actionTriggered = new EventEmitter();
|
|
1557
|
-
get controlId() {
|
|
1558
|
-
return this.id ?? this.generatedId;
|
|
1559
|
-
}
|
|
1560
|
-
onChange = () => undefined;
|
|
1561
|
-
onTouched = () => undefined;
|
|
1562
|
-
writeValue(value) {
|
|
1563
|
-
this.value = value ?? '';
|
|
1564
|
-
this.cdr.markForCheck();
|
|
1290
|
+
writeValue(value) {
|
|
1291
|
+
this.value = value ?? '';
|
|
1292
|
+
this.cdr.markForCheck();
|
|
1565
1293
|
}
|
|
1566
1294
|
registerOnChange(fn) {
|
|
1567
1295
|
this.onChange = fn;
|
|
@@ -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,7 +1740,16 @@ class UiPhotoInputComponent {
|
|
|
2090
1740
|
previews = [];
|
|
2091
1741
|
uploading = false;
|
|
2092
1742
|
fileTypes = new Map();
|
|
2093
|
-
|
|
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; }
|
|
1752
|
+
ngOnChanges(changes) {
|
|
2094
1753
|
if (changes['value'] || changes['multiple'])
|
|
2095
1754
|
this.rebuildPreviews();
|
|
2096
1755
|
}
|
|
@@ -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();
|
|
2040
|
+
}
|
|
2041
|
+
markTouched() {
|
|
2042
|
+
this.onTouched();
|
|
2043
|
+
}
|
|
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);
|
|
2297
2224
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
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
|
+
};
|
|
2300
2231
|
}
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
if (
|
|
2304
|
-
|
|
2305
|
-
}
|
|
2306
|
-
return
|
|
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 */ []));
|
|
@@ -2921,12 +3183,8 @@ class UiActionFormModalComponent {
|
|
|
2921
3183
|
this.loadForm();
|
|
2922
3184
|
}
|
|
2923
3185
|
}
|
|
2924
|
-
close() {
|
|
2925
|
-
|
|
2926
|
-
}
|
|
2927
|
-
onValueChange(value) {
|
|
2928
|
-
this.formValue.set(value);
|
|
2929
|
-
}
|
|
3186
|
+
close() { this.closed.emit(); }
|
|
3187
|
+
onValueChange(value) { this.formValue.set(value); }
|
|
2930
3188
|
onLookupCompleted(event) {
|
|
2931
3189
|
const redirectTo = String(this.formConfig()?.redirectTo || '').trim();
|
|
2932
3190
|
if (!redirectTo)
|
|
@@ -2938,24 +3196,30 @@ class UiActionFormModalComponent {
|
|
|
2938
3196
|
void this.router.navigateByUrl(target.startsWith('/') ? target : `/${target}`);
|
|
2939
3197
|
}
|
|
2940
3198
|
}
|
|
2941
|
-
onUploadingChange(uploading) {
|
|
2942
|
-
this.isUploading.set(uploading);
|
|
2943
|
-
}
|
|
3199
|
+
onUploadingChange(uploading) { this.isUploading.set(uploading); }
|
|
2944
3200
|
onFieldActionTriggered(event) {
|
|
3201
|
+
if (this.isActionRunning() || this.isSubmitting())
|
|
3202
|
+
return;
|
|
2945
3203
|
const payload = this.normalizePayload({ ...this.formValue(), ...(event.action.payload || {}) });
|
|
2946
3204
|
const endpoint = this.resolveTemplate(this.resolveEndpoint(event.action.api), payload);
|
|
2947
3205
|
if (!endpoint)
|
|
2948
3206
|
return;
|
|
3207
|
+
this.isActionRunning.set(true);
|
|
2949
3208
|
this.http.request(this.resolveMethod(event.action.method), endpoint, { body: payload }).subscribe({
|
|
2950
3209
|
next: (response) => {
|
|
2951
3210
|
const data = this.extractData(response);
|
|
2952
3211
|
if (data)
|
|
2953
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);
|
|
2954
3218
|
},
|
|
2955
3219
|
});
|
|
2956
3220
|
}
|
|
2957
3221
|
confirm() {
|
|
2958
|
-
if (this.isSubmitting())
|
|
3222
|
+
if (this.isSubmitting() || this.isActionRunning() || this.isUploading())
|
|
2959
3223
|
return;
|
|
2960
3224
|
const action = this.submitAction();
|
|
2961
3225
|
const endpointTemplate = this.formConfig()?.api || action?.api;
|
|
@@ -3003,10 +3267,7 @@ class UiActionFormModalComponent {
|
|
|
3003
3267
|
const config = {
|
|
3004
3268
|
...raw,
|
|
3005
3269
|
title: raw.title || raw.meta?.title,
|
|
3006
|
-
actions: (raw.actions || []).map((action) => ({
|
|
3007
|
-
...action,
|
|
3008
|
-
type: this.normalizeActionType(action.type),
|
|
3009
|
-
})),
|
|
3270
|
+
actions: (raw.actions || []).map((action) => ({ ...action, type: this.normalizeActionType(action.type) })),
|
|
3010
3271
|
};
|
|
3011
3272
|
this.formConfig.set(config);
|
|
3012
3273
|
this.formValue.set({ ...(config.defaults || {}), ...(config.item || {}), ...(this.initialValue() || {}), ...(recordId ? { id: recordId } : {}) });
|
|
@@ -3031,9 +3292,8 @@ class UiActionFormModalComponent {
|
|
|
3031
3292
|
if (field.type !== 'photo' || !field.multiple)
|
|
3032
3293
|
continue;
|
|
3033
3294
|
const value = payload[field.id];
|
|
3034
|
-
if (typeof value === 'string')
|
|
3295
|
+
if (typeof value === 'string')
|
|
3035
3296
|
payload[field.id] = value.split(',').map((item) => item.trim()).filter(Boolean);
|
|
3036
|
-
}
|
|
3037
3297
|
}
|
|
3038
3298
|
return payload;
|
|
3039
3299
|
}
|
|
@@ -3085,15 +3345,15 @@ class UiActionFormModalComponent {
|
|
|
3085
3345
|
return value == null || value === '' ? '' : encodeURIComponent(String(value));
|
|
3086
3346
|
});
|
|
3087
3347
|
}
|
|
3088
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3089
|
-
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"] }] });
|
|
3090
3350
|
}
|
|
3091
|
-
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: [{
|
|
3092
3352
|
type: Component,
|
|
3093
|
-
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" }]
|
|
3094
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"] }] } });
|
|
3095
3355
|
|
|
3096
|
-
class
|
|
3356
|
+
class InputComponent {
|
|
3097
3357
|
cdr = inject(ChangeDetectorRef);
|
|
3098
3358
|
label;
|
|
3099
3359
|
hint;
|
|
@@ -3106,14 +3366,13 @@ class UiInputComponent {
|
|
|
3106
3366
|
required = false;
|
|
3107
3367
|
fullWidth = false;
|
|
3108
3368
|
autocomplete;
|
|
3109
|
-
readonly = false;
|
|
3110
3369
|
pattern;
|
|
3111
3370
|
error;
|
|
3112
3371
|
actions = [];
|
|
3113
3372
|
disabled = false;
|
|
3114
3373
|
value = '';
|
|
3115
3374
|
static nextId = 0;
|
|
3116
|
-
generatedId = `
|
|
3375
|
+
generatedId = `input-${InputComponent.nextId++}`;
|
|
3117
3376
|
actionTriggered = new EventEmitter();
|
|
3118
3377
|
get controlId() {
|
|
3119
3378
|
return this.id ?? this.generatedId;
|
|
@@ -3142,24 +3401,24 @@ class UiInputComponent {
|
|
|
3142
3401
|
triggerAction(action) {
|
|
3143
3402
|
this.actionTriggered.emit(action);
|
|
3144
3403
|
}
|
|
3145
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3146
|
-
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: [
|
|
3147
3406
|
{
|
|
3148
3407
|
provide: NG_VALUE_ACCESSOR,
|
|
3149
|
-
useExisting: forwardRef(() =>
|
|
3408
|
+
useExisting: forwardRef(() => InputComponent),
|
|
3150
3409
|
multi: true,
|
|
3151
3410
|
},
|
|
3152
|
-
], 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 });
|
|
3153
3412
|
}
|
|
3154
|
-
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: [{
|
|
3155
3414
|
type: Component,
|
|
3156
|
-
args: [{ selector: '
|
|
3415
|
+
args: [{ selector: 'input-base', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
3157
3416
|
{
|
|
3158
3417
|
provide: NG_VALUE_ACCESSOR,
|
|
3159
|
-
useExisting: forwardRef(() =>
|
|
3418
|
+
useExisting: forwardRef(() => InputComponent),
|
|
3160
3419
|
multi: true,
|
|
3161
3420
|
},
|
|
3162
|
-
], 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" }]
|
|
3163
3422
|
}], propDecorators: { label: [{
|
|
3164
3423
|
type: Input
|
|
3165
3424
|
}], hint: [{
|
|
@@ -3182,8 +3441,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3182
3441
|
type: Input
|
|
3183
3442
|
}], autocomplete: [{
|
|
3184
3443
|
type: Input
|
|
3185
|
-
}], readonly: [{
|
|
3186
|
-
type: Input
|
|
3187
3444
|
}], pattern: [{
|
|
3188
3445
|
type: Input
|
|
3189
3446
|
}], error: [{
|
|
@@ -3194,7 +3451,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3194
3451
|
type: Output
|
|
3195
3452
|
}] } });
|
|
3196
3453
|
|
|
3197
|
-
class
|
|
3454
|
+
class InputPhotoGalleryComponent {
|
|
3198
3455
|
http = inject(HttpClient);
|
|
3199
3456
|
label = 'Фото';
|
|
3200
3457
|
orderLabel = 'Порядок фото (перетягуй)';
|
|
@@ -3336,12 +3593,12 @@ class UiPhotoGalleryFieldComponent {
|
|
|
3336
3593
|
formData.append('file', file);
|
|
3337
3594
|
return this.http.post('/api/files/', formData).pipe(map((response) => ({ imageId: response.id })));
|
|
3338
3595
|
}
|
|
3339
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3340
|
-
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"] }] });
|
|
3341
3598
|
}
|
|
3342
|
-
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: [{
|
|
3343
3600
|
type: Component,
|
|
3344
|
-
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"] }]
|
|
3345
3602
|
}], propDecorators: { label: [{
|
|
3346
3603
|
type: Input
|
|
3347
3604
|
}], orderLabel: [{
|
|
@@ -3360,7 +3617,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3360
3617
|
type: Output
|
|
3361
3618
|
}] } });
|
|
3362
3619
|
|
|
3363
|
-
class
|
|
3620
|
+
class InputRangeSliderComponent {
|
|
3364
3621
|
min = 0;
|
|
3365
3622
|
max = 100;
|
|
3366
3623
|
step = 1;
|
|
@@ -3449,8 +3706,8 @@ class UiRangeSliderComponent {
|
|
|
3449
3706
|
input.step = String(this.safeStep);
|
|
3450
3707
|
input.value = String(value);
|
|
3451
3708
|
}
|
|
3452
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type:
|
|
3453
|
-
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: `
|
|
3454
3711
|
<div class="range-slider">
|
|
3455
3712
|
<div class="range-slider__track"></div>
|
|
3456
3713
|
<div
|
|
@@ -3485,9 +3742,9 @@ class UiRangeSliderComponent {
|
|
|
3485
3742
|
</div>
|
|
3486
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 }] });
|
|
3487
3744
|
}
|
|
3488
|
-
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: [{
|
|
3489
3746
|
type: Component,
|
|
3490
|
-
args: [{ selector: '
|
|
3747
|
+
args: [{ selector: 'input-range-slider', standalone: true, imports: [CommonModule], template: `
|
|
3491
3748
|
<div class="range-slider">
|
|
3492
3749
|
<div class="range-slider__track"></div>
|
|
3493
3750
|
<div
|
|
@@ -3553,5 +3810,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
|
|
|
3553
3810
|
* Generated bundle index. Do not edit.
|
|
3554
3811
|
*/
|
|
3555
3812
|
|
|
3556
|
-
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 };
|
|
3557
3814
|
//# sourceMappingURL=maro-tech-form.mjs.map
|