@maro-tech/form 0.0.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 +40 -0
- package/fesm2022/maro-tech-form.mjs +3191 -0
- package/fesm2022/maro-tech-form.mjs.map +1 -0
- package/package.json +31 -0
- package/types/maro-tech-form.d.ts +909 -0
|
@@ -0,0 +1,3191 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, Injectable, inject, InjectionToken, EventEmitter, Output, Input, Component, input, output, computed, ChangeDetectorRef, forwardRef, ChangeDetectionStrategy, ViewContainerRef, ViewChild, DestroyRef, ElementRef, booleanAttribute } from '@angular/core';
|
|
3
|
+
import { HttpClient } from '@angular/common/http';
|
|
4
|
+
import * as i1 from '@angular/common';
|
|
5
|
+
import { CommonModule } from '@angular/common';
|
|
6
|
+
import * as i2 from '@angular/forms';
|
|
7
|
+
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
8
|
+
import { forkJoin, Observable, from, Subject, debounceTime, distinctUntilChanged, switchMap, of } from 'rxjs';
|
|
9
|
+
import { UiBarcodeScannerInputComponent as UiBarcodeScannerInputComponent$1 } from '@forms/inputs/barcode/ui-barcode-scanner-input.component';
|
|
10
|
+
import { UiSelectComponent as UiSelectComponent$1 } from '@forms/inputs/select/ui-select.component';
|
|
11
|
+
import { UiFormInputHostComponent as UiFormInputHostComponent$1 } from '@forms/inputs/form-input-host.component';
|
|
12
|
+
import { UiFileInputComponent as UiFileInputComponent$1 } from '@forms/inputs/file/ui-file-input.component';
|
|
13
|
+
import { UiPhotoInputComponent as UiPhotoInputComponent$1 } from '@forms/inputs/photo/ui-photo-input.component';
|
|
14
|
+
import { UiCheckboxInputComponent as UiCheckboxInputComponent$1 } from '@forms/inputs/checkbox/ui-checkbox-input.component';
|
|
15
|
+
import { UiColorInputComponent as UiColorInputComponent$1 } from '@forms/inputs/color/ui-color-input.component';
|
|
16
|
+
import { UiSlugInputComponent as UiSlugInputComponent$1 } from '@forms/inputs/slug/ui-slug-input.component';
|
|
17
|
+
import { UiFormContext as UiFormContext$1 } from '@forms/context/form-context.service';
|
|
18
|
+
import { MaroFormsApiService as MaroFormsApiService$1 } from '@forms/forms-api.service';
|
|
19
|
+
import { UiButtonComponent } from '@maro-tech/ui';
|
|
20
|
+
import { Router } from '@angular/router';
|
|
21
|
+
import { UiDynamicFormComponent as UiDynamicFormComponent$1 } from '@forms/dynamic/ui-dynamic-form.component';
|
|
22
|
+
import { BrowserMultiFormatOneDReader } from '@zxing/browser';
|
|
23
|
+
import { DecodeHintType, BarcodeFormat } from '@zxing/library';
|
|
24
|
+
import * as i1$1 from '@angular/cdk/drag-drop';
|
|
25
|
+
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
|
|
26
|
+
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
|
27
|
+
import * as i1$2 from 'lucide-angular';
|
|
28
|
+
import { X, ChevronDown, Check, LucideAngularModule } from 'lucide-angular';
|
|
29
|
+
|
|
30
|
+
class UiFormContext {
|
|
31
|
+
value = signal({}, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
32
|
+
errors = signal({}, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
33
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
34
|
+
disabled = signal(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
35
|
+
setValue(value) {
|
|
36
|
+
this.value.set({ ...value });
|
|
37
|
+
}
|
|
38
|
+
patchValue(value) {
|
|
39
|
+
this.value.update((current) => ({ ...current, ...value }));
|
|
40
|
+
}
|
|
41
|
+
setErrors(errors) {
|
|
42
|
+
this.errors.set({ ...errors });
|
|
43
|
+
}
|
|
44
|
+
reset(value = {}) {
|
|
45
|
+
this.value.set({ ...value });
|
|
46
|
+
this.errors.set({});
|
|
47
|
+
this.loading.set(false);
|
|
48
|
+
}
|
|
49
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
50
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormContext });
|
|
51
|
+
}
|
|
52
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormContext, decorators: [{
|
|
53
|
+
type: Injectable
|
|
54
|
+
}] });
|
|
55
|
+
|
|
56
|
+
class MaroFormsApiService {
|
|
57
|
+
http = inject(HttpClient);
|
|
58
|
+
getSuggestByName(name, query = '') {
|
|
59
|
+
const suffix = query ? `?search=${encodeURIComponent(query)}` : '';
|
|
60
|
+
return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
|
|
61
|
+
}
|
|
62
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: MaroFormsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
63
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: MaroFormsApiService, providedIn: 'root' });
|
|
64
|
+
}
|
|
65
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: MaroFormsApiService, decorators: [{
|
|
66
|
+
type: Injectable,
|
|
67
|
+
args: [{ providedIn: 'root' }]
|
|
68
|
+
}] });
|
|
69
|
+
|
|
70
|
+
class UiFormLookupService {
|
|
71
|
+
http = inject(HttpClient);
|
|
72
|
+
lookup(field, value) {
|
|
73
|
+
const api = String(field.lookupApi || '').trim();
|
|
74
|
+
const endpoint = api
|
|
75
|
+
.replace(/:value/g, encodeURIComponent(value))
|
|
76
|
+
.replace(new RegExp(`:${field.id}\\b`, 'g'), encodeURIComponent(value));
|
|
77
|
+
if (/^https?:\/\//i.test(endpoint)) {
|
|
78
|
+
return this.http.get(endpoint);
|
|
79
|
+
}
|
|
80
|
+
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
81
|
+
return this.http.get(`/api${path}`);
|
|
82
|
+
}
|
|
83
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormLookupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
84
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormLookupService });
|
|
85
|
+
}
|
|
86
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormLookupService, decorators: [{
|
|
87
|
+
type: Injectable
|
|
88
|
+
}] });
|
|
89
|
+
|
|
90
|
+
const UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS = new InjectionToken('UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS');
|
|
91
|
+
|
|
92
|
+
const isDynamicFormContainerField = (field) => field.type === 'container';
|
|
93
|
+
const isDynamicFormTemplateField = (field) => field.type === 'template';
|
|
94
|
+
const flattenDynamicFormFields = (fields) => {
|
|
95
|
+
const result = [];
|
|
96
|
+
for (const field of fields || []) {
|
|
97
|
+
if (!field) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
result.push(field);
|
|
101
|
+
if (isDynamicFormContainerField(field) && Array.isArray(field.fields) && field.fields.length) {
|
|
102
|
+
result.push(...flattenDynamicFormFields(field.fields));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
class UiDynamicFormComponent {
|
|
109
|
+
formsApi = inject(MaroFormsApiService$1);
|
|
110
|
+
lookupService = inject(UiFormLookupService);
|
|
111
|
+
formContext = inject(UiFormContext$1);
|
|
112
|
+
optionsSourceResolvers = inject(UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS, { optional: true }) ?? [];
|
|
113
|
+
fields = [];
|
|
114
|
+
value = {};
|
|
115
|
+
errors = {};
|
|
116
|
+
submitLabel = 'Submit';
|
|
117
|
+
showSubmit = true;
|
|
118
|
+
disabled = false;
|
|
119
|
+
uploadPhoto = null;
|
|
120
|
+
valueChange = new EventEmitter();
|
|
121
|
+
lookupCompleted = new EventEmitter();
|
|
122
|
+
submitted = new EventEmitter();
|
|
123
|
+
filesChange = new EventEmitter();
|
|
124
|
+
uploadingChange = new EventEmitter();
|
|
125
|
+
fieldActionTriggered = new EventEmitter();
|
|
126
|
+
modelSignal = signal({}, ...(ngDevMode ? [{ debugName: "modelSignal" }] : /* istanbul ignore next */ []));
|
|
127
|
+
dictionaryOptionsSignal = signal({}, ...(ngDevMode ? [{ debugName: "dictionaryOptionsSignal" }] : /* istanbul ignore next */ []));
|
|
128
|
+
lookupLoadingSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupLoadingSignal" }] : /* istanbul ignore next */ []));
|
|
129
|
+
lookupErrorsSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupErrorsSignal" }] : /* istanbul ignore next */ []));
|
|
130
|
+
selectSearchChanges = new Map();
|
|
131
|
+
selectSearchSubscriptions = new Map();
|
|
132
|
+
optionsSourceRequestVersions = new Map();
|
|
133
|
+
ngOnInit() {
|
|
134
|
+
this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
|
|
135
|
+
}
|
|
136
|
+
ngOnChanges(changes) {
|
|
137
|
+
if ('fields' in changes || 'value' in changes) {
|
|
138
|
+
const next = { ...this.value };
|
|
139
|
+
for (const field of this.getLeafFields()) {
|
|
140
|
+
const defaultValue = field.type === 'checkbox' ? false : '';
|
|
141
|
+
const nextValue = this.value[field.id] !== undefined
|
|
142
|
+
? this.value[field.id]
|
|
143
|
+
: field.default !== undefined
|
|
144
|
+
? field.default
|
|
145
|
+
: defaultValue;
|
|
146
|
+
next[field.id] = nextValue;
|
|
147
|
+
}
|
|
148
|
+
this.modelSignal.set(next);
|
|
149
|
+
this.formContext.setValue(next);
|
|
150
|
+
this.formContext.setErrors(this.errors);
|
|
151
|
+
this.formContext.disabled.set(this.disabled);
|
|
152
|
+
}
|
|
153
|
+
if ('fields' in changes || 'value' in changes) {
|
|
154
|
+
this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
ngOnDestroy() {
|
|
158
|
+
this.cleanupSelectSearchStreams();
|
|
159
|
+
}
|
|
160
|
+
updateField(fieldId, nextValue) {
|
|
161
|
+
this.modelSignal.update((current) => {
|
|
162
|
+
const updated = { ...current, [fieldId]: nextValue };
|
|
163
|
+
this.formContext.setValue(updated);
|
|
164
|
+
this.valueChange.emit(updated);
|
|
165
|
+
this.ensureOptionsSourceOptionsLoaded(updated);
|
|
166
|
+
return updated;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
onBarcodeValueChange(field, value) {
|
|
170
|
+
this.updateField(field.id, value);
|
|
171
|
+
this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
|
|
172
|
+
}
|
|
173
|
+
onBarcodeScanned(field, value) {
|
|
174
|
+
this.updateField(field.id, value);
|
|
175
|
+
this.runFieldLookup(field, value);
|
|
176
|
+
}
|
|
177
|
+
onSelectValueChange(field, value) {
|
|
178
|
+
this.updateField(field.id, value);
|
|
179
|
+
const nextValue = Array.isArray(value) ? '' : String(value || '').trim();
|
|
180
|
+
if (field.lookupApi) {
|
|
181
|
+
this.runFieldLookup(field, nextValue);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
|
|
185
|
+
}
|
|
186
|
+
runBarcodeLookup(field) {
|
|
187
|
+
this.runFieldLookup(field);
|
|
188
|
+
}
|
|
189
|
+
runFieldLookup(field, overrideValue) {
|
|
190
|
+
const api = String(field.lookupApi || '').trim();
|
|
191
|
+
if (!api)
|
|
192
|
+
return;
|
|
193
|
+
const value = String(overrideValue ?? this.getStringValue(field.id)).trim();
|
|
194
|
+
if (!value) {
|
|
195
|
+
this.lookupErrorsSignal.update((current) => ({
|
|
196
|
+
...current,
|
|
197
|
+
[field.id]: field.type === 'select' ? 'Вкажіть значення для пошуку' : 'Вкажіть штрихкод для пошуку',
|
|
198
|
+
}));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.lookupLoadingSignal.update((current) => ({ ...current, [field.id]: true }));
|
|
202
|
+
this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
|
|
203
|
+
this.lookupService.lookup(field, value).subscribe({
|
|
204
|
+
next: (response) => {
|
|
205
|
+
this.applyLookupResponse(field, response || {});
|
|
206
|
+
this.lookupCompleted.emit({ fieldId: field.id, response: response || {} });
|
|
207
|
+
this.lookupLoadingSignal.update((current) => ({ ...current, [field.id]: false }));
|
|
208
|
+
},
|
|
209
|
+
error: (error) => {
|
|
210
|
+
const message = error?.error?.message || 'Не вдалося знайти запис';
|
|
211
|
+
this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: message }));
|
|
212
|
+
this.lookupLoadingSignal.update((current) => ({ ...current, [field.id]: false }));
|
|
213
|
+
const errorField = String(field.lookupErrorField || '').trim();
|
|
214
|
+
if (errorField) {
|
|
215
|
+
this.updateField(errorField, message);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
onFieldActionTriggered(fieldId, action) {
|
|
221
|
+
this.fieldActionTriggered.emit({ fieldId, action });
|
|
222
|
+
}
|
|
223
|
+
resolveFieldActions(field) {
|
|
224
|
+
const model = this.modelSignal();
|
|
225
|
+
return (field.actions || []).filter((action) => {
|
|
226
|
+
const condition = String(action.condition || '').trim();
|
|
227
|
+
if (!condition) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const evaluator = new Function('model', `with (model) { return !!(${condition}); }`);
|
|
232
|
+
return evaluator(model);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
getActions(field) {
|
|
240
|
+
return this.resolveFieldActions(field);
|
|
241
|
+
}
|
|
242
|
+
isFieldDisabled(field) {
|
|
243
|
+
if (this.disabled)
|
|
244
|
+
return true;
|
|
245
|
+
if (typeof field.disabled === 'boolean')
|
|
246
|
+
return field.disabled;
|
|
247
|
+
const raw = String(field.disabled || '').trim();
|
|
248
|
+
if (!raw)
|
|
249
|
+
return false;
|
|
250
|
+
const condition = raw.startsWith('${') && raw.endsWith('}') ? raw.slice(2, -1).trim() : raw;
|
|
251
|
+
try {
|
|
252
|
+
return Boolean(Function('model', `with (model) { return Boolean(${condition}); }`)(this.modelSignal()));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
submitForm() {
|
|
259
|
+
this.submitted.emit(this.modelSignal());
|
|
260
|
+
}
|
|
261
|
+
isTextArea(field) {
|
|
262
|
+
return (field.type ?? 'text') === 'textarea';
|
|
263
|
+
}
|
|
264
|
+
isInputField(field) {
|
|
265
|
+
const type = field.type ?? 'text';
|
|
266
|
+
return type === 'text' || type === 'number' || type === 'date' || type === 'datetime' || type === 'email' || type === 'password' || type === 'textarea';
|
|
267
|
+
}
|
|
268
|
+
isBarcodeField(field) {
|
|
269
|
+
return field.type === 'barcode';
|
|
270
|
+
}
|
|
271
|
+
getStringValue(fieldId) {
|
|
272
|
+
return String(this.modelSignal()[fieldId] || '');
|
|
273
|
+
}
|
|
274
|
+
getLookupError(fieldId) {
|
|
275
|
+
return this.lookupErrorsSignal()[fieldId] || '';
|
|
276
|
+
}
|
|
277
|
+
isLookupLoading(fieldId) {
|
|
278
|
+
return !!this.lookupLoadingSignal()[fieldId];
|
|
279
|
+
}
|
|
280
|
+
resolveClassName(value) {
|
|
281
|
+
return String(value || '').trim();
|
|
282
|
+
}
|
|
283
|
+
isContainerField(field) {
|
|
284
|
+
return isDynamicFormContainerField(field);
|
|
285
|
+
}
|
|
286
|
+
isTemplateField(field) {
|
|
287
|
+
return isDynamicFormTemplateField(field);
|
|
288
|
+
}
|
|
289
|
+
isFieldVisible(field) {
|
|
290
|
+
if (typeof field.hidden === 'boolean') {
|
|
291
|
+
return !field.hidden;
|
|
292
|
+
}
|
|
293
|
+
const hidden = String(field.hidden || '').trim();
|
|
294
|
+
if (!hidden) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
const expression = hidden.startsWith('${') && hidden.endsWith('}')
|
|
298
|
+
? hidden.slice(2, -1)
|
|
299
|
+
: hidden;
|
|
300
|
+
try {
|
|
301
|
+
const model = this.modelSignal();
|
|
302
|
+
const evaluator = new Function('model', `with (model) { return !!(${expression}); }`);
|
|
303
|
+
return !evaluator(model);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
resolveInputType(field) {
|
|
310
|
+
const type = field.type ?? 'text';
|
|
311
|
+
if (type === 'number' || type === 'date' || type === 'email' || type === 'password') {
|
|
312
|
+
return type;
|
|
313
|
+
}
|
|
314
|
+
if (type === 'datetime')
|
|
315
|
+
return 'datetime-local';
|
|
316
|
+
return 'text';
|
|
317
|
+
}
|
|
318
|
+
resolveSelectOptions(field) {
|
|
319
|
+
if (field.options?.length) {
|
|
320
|
+
return field.options;
|
|
321
|
+
}
|
|
322
|
+
const sourceKey = String(field.optionsSource || '').trim();
|
|
323
|
+
if (!sourceKey) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
return this.dictionaryOptionsSignal()[sourceKey] ?? [];
|
|
327
|
+
}
|
|
328
|
+
renderTemplate(field) {
|
|
329
|
+
const template = this.isTemplateField(field) ? field.template : '';
|
|
330
|
+
if (!template) {
|
|
331
|
+
return '';
|
|
332
|
+
}
|
|
333
|
+
const model = this.modelSignal();
|
|
334
|
+
const context = this.isTemplateField(field) ? { ...(field.templateContext || {}), ...model } : model;
|
|
335
|
+
return String(template).replace(/\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g, (_match, path) => {
|
|
336
|
+
const value = this.resolveTemplateValue(context, path);
|
|
337
|
+
return value == null ? '' : String(value);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
getContainerTag(field) {
|
|
341
|
+
if (!this.isContainerField(field) && !this.isTemplateField(field)) {
|
|
342
|
+
return 'div';
|
|
343
|
+
}
|
|
344
|
+
return field.tag || 'section';
|
|
345
|
+
}
|
|
346
|
+
getContainerClass(field) {
|
|
347
|
+
const className = String(field.className || '').trim();
|
|
348
|
+
const tag = this.getContainerTag(field);
|
|
349
|
+
return [
|
|
350
|
+
this.resolveColSpanClass(field),
|
|
351
|
+
tag === 'aside'
|
|
352
|
+
? 'space-y-4 rounded-3xl border border-slate-200 bg-white p-4 shadow-sm'
|
|
353
|
+
: 'rounded-3xl border border-slate-200 bg-white p-6 shadow-sm',
|
|
354
|
+
className,
|
|
355
|
+
].filter(Boolean).join(' ');
|
|
356
|
+
}
|
|
357
|
+
getContainerContentClass(field) {
|
|
358
|
+
const className = String(field.contentClassName || '').trim();
|
|
359
|
+
const layout = this.isContainerField(field) ? field.layout : undefined;
|
|
360
|
+
return [
|
|
361
|
+
layout === 'stack' ? 'space-y-4' : 'grid grid-cols-1 gap-4 md:grid-cols-12',
|
|
362
|
+
className,
|
|
363
|
+
].filter(Boolean).join(' ');
|
|
364
|
+
}
|
|
365
|
+
getFieldClass(field) {
|
|
366
|
+
const span = Math.min(12, Math.max(1, Number(field.col ?? 12)));
|
|
367
|
+
return {
|
|
368
|
+
'md:col-span-1': span === 1,
|
|
369
|
+
'md:col-span-2': span === 2,
|
|
370
|
+
'md:col-span-3': span === 3,
|
|
371
|
+
'md:col-span-4': span === 4,
|
|
372
|
+
'md:col-span-5': span === 5,
|
|
373
|
+
'md:col-span-6': span === 6,
|
|
374
|
+
'md:col-span-7': span === 7,
|
|
375
|
+
'md:col-span-8': span === 8,
|
|
376
|
+
'md:col-span-9': span === 9,
|
|
377
|
+
'md:col-span-10': span === 10,
|
|
378
|
+
'md:col-span-11': span === 11,
|
|
379
|
+
'md:col-span-12': span === 12,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
resolveColSpanClass(field) {
|
|
383
|
+
const span = Math.min(12, Math.max(1, Number(field.col ?? 12)));
|
|
384
|
+
const classes = {
|
|
385
|
+
1: 'md:col-span-1',
|
|
386
|
+
2: 'md:col-span-2',
|
|
387
|
+
3: 'md:col-span-3',
|
|
388
|
+
4: 'md:col-span-4',
|
|
389
|
+
5: 'md:col-span-5',
|
|
390
|
+
6: 'md:col-span-6',
|
|
391
|
+
7: 'md:col-span-7',
|
|
392
|
+
8: 'md:col-span-8',
|
|
393
|
+
9: 'md:col-span-9',
|
|
394
|
+
10: 'md:col-span-10',
|
|
395
|
+
11: 'md:col-span-11',
|
|
396
|
+
12: 'md:col-span-12',
|
|
397
|
+
};
|
|
398
|
+
return classes[span] || classes[12];
|
|
399
|
+
}
|
|
400
|
+
onSelectSearchChange(field, query) {
|
|
401
|
+
const key = String(field.optionsSource || '').trim();
|
|
402
|
+
if (!key)
|
|
403
|
+
return;
|
|
404
|
+
this.getSelectSearchStream(field, key).next(String(query ?? ''));
|
|
405
|
+
}
|
|
406
|
+
ensureOptionsSourceOptionsLoaded(model) {
|
|
407
|
+
if (!this.optionsSourceResolvers.length) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const sourceKeys = [...new Set(this.getLeafFields()
|
|
411
|
+
.filter((field) => (field.type ?? 'text') === 'select' && !!field.optionsSource)
|
|
412
|
+
.map((field) => String(field.optionsSource || '').trim())
|
|
413
|
+
.filter(Boolean))];
|
|
414
|
+
if (!sourceKeys.length) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
for (const sourceKey of sourceKeys) {
|
|
418
|
+
const resolver = this.optionsSourceResolvers.find((item) => item.source === sourceKey);
|
|
419
|
+
if (!resolver) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const sourceFields = this.getLeafFields().filter((field) => String(field.optionsSource || '').trim() === sourceKey);
|
|
423
|
+
if (!sourceFields.length) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const version = (this.optionsSourceRequestVersions.get(sourceKey) || 0) + 1;
|
|
427
|
+
this.optionsSourceRequestVersions.set(sourceKey, version);
|
|
428
|
+
forkJoin(sourceFields.map((field) => {
|
|
429
|
+
const resolved = resolver.resolve({ source: sourceKey, field, model });
|
|
430
|
+
return resolved instanceof Observable ? resolved : from(Promise.resolve(resolved));
|
|
431
|
+
})).subscribe({
|
|
432
|
+
next: (results) => {
|
|
433
|
+
if ((this.optionsSourceRequestVersions.get(sourceKey) || 0) !== version) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const options = results
|
|
437
|
+
.flat()
|
|
438
|
+
.map((item) => ({
|
|
439
|
+
label: String(item?.label ?? ''),
|
|
440
|
+
value: item?.value == null ? null : String(item.value),
|
|
441
|
+
disabled: item?.disabled,
|
|
442
|
+
imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
|
|
443
|
+
}))
|
|
444
|
+
.filter((item) => !!item.label || item.value !== null);
|
|
445
|
+
this.dictionaryOptionsSignal.update((current) => ({
|
|
446
|
+
...current,
|
|
447
|
+
[sourceKey]: options,
|
|
448
|
+
}));
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
getSelectSearchStream(field, key) {
|
|
454
|
+
const normalizedKey = String(key || '').trim();
|
|
455
|
+
const existing = this.selectSearchChanges.get(normalizedKey);
|
|
456
|
+
if (existing) {
|
|
457
|
+
return existing;
|
|
458
|
+
}
|
|
459
|
+
const search$ = new Subject();
|
|
460
|
+
this.selectSearchChanges.set(normalizedKey, search$);
|
|
461
|
+
const subscription = search$
|
|
462
|
+
.pipe(debounceTime(300), distinctUntilChanged(), switchMap((query) => {
|
|
463
|
+
const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
|
|
464
|
+
if (resolver) {
|
|
465
|
+
return resolver.resolve({ source: normalizedKey, field, model: this.modelSignal(), query });
|
|
466
|
+
}
|
|
467
|
+
return this.formsApi.getSuggestByName(normalizedKey, query);
|
|
468
|
+
}))
|
|
469
|
+
.subscribe({
|
|
470
|
+
next: (items) => {
|
|
471
|
+
const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
|
|
472
|
+
this.dictionaryOptionsSignal.update((current) => ({
|
|
473
|
+
...current,
|
|
474
|
+
[normalizedKey]: resolver
|
|
475
|
+
? (items ?? []).map((item) => ({
|
|
476
|
+
label: String(item?.label ?? ''),
|
|
477
|
+
value: item?.value == null ? null : String(item.value),
|
|
478
|
+
disabled: item?.disabled,
|
|
479
|
+
imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
|
|
480
|
+
}))
|
|
481
|
+
: (items ?? []).map((item) => ({
|
|
482
|
+
label: String(item?.label ?? ''),
|
|
483
|
+
value: item?.['key'] != null ? String(item['key']) : null,
|
|
484
|
+
})),
|
|
485
|
+
}));
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
this.selectSearchSubscriptions.set(normalizedKey, subscription);
|
|
489
|
+
return search$;
|
|
490
|
+
}
|
|
491
|
+
cleanupSelectSearchStreams() {
|
|
492
|
+
for (const subscription of this.selectSearchSubscriptions.values()) {
|
|
493
|
+
subscription.unsubscribe();
|
|
494
|
+
}
|
|
495
|
+
this.selectSearchSubscriptions.clear();
|
|
496
|
+
this.selectSearchChanges.clear();
|
|
497
|
+
}
|
|
498
|
+
applyLookupResponse(field, response) {
|
|
499
|
+
const lookupMap = field.lookupMap || {};
|
|
500
|
+
const mapped = Object.entries(lookupMap).reduce((acc, [targetField, sourcePath]) => {
|
|
501
|
+
acc[targetField] = this.resolveTemplateValue(response, sourcePath);
|
|
502
|
+
return acc;
|
|
503
|
+
}, {});
|
|
504
|
+
this.modelSignal.update((current) => {
|
|
505
|
+
const updated = { ...current, ...mapped };
|
|
506
|
+
this.formContext.setValue(updated);
|
|
507
|
+
this.valueChange.emit(updated);
|
|
508
|
+
this.ensureOptionsSourceOptionsLoaded(updated);
|
|
509
|
+
return updated;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
resolveTemplateValue(model, pathRaw) {
|
|
513
|
+
const path = String(pathRaw || '').trim();
|
|
514
|
+
if (!path) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
return path.split('.').reduce((current, part) => {
|
|
518
|
+
if (current == null || typeof current !== 'object') {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
return current[part];
|
|
522
|
+
}, model);
|
|
523
|
+
}
|
|
524
|
+
getFlatFields() {
|
|
525
|
+
return flattenDynamicFormFields(this.fields);
|
|
526
|
+
}
|
|
527
|
+
getLeafFields() {
|
|
528
|
+
return this.getFlatFields().filter((field) => field.type !== 'container' && field.type !== 'template');
|
|
529
|
+
}
|
|
530
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
531
|
+
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", uploadPhoto: "uploadPhoto" }, outputs: { valueChange: "valueChange", lookupCompleted: "lookupCompleted", submitted: "submitted", filesChange: "filesChange", uploadingChange: "uploadingChange", fieldActionTriggered: "fieldActionTriggered" }, providers: [UiFormContext$1, 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 [uploadPhoto]=\"uploadPhoto\"\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.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.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$1, selector: "forms-form-input-host", inputs: ["field", "value", "error", "disabled", "actions", "options"], outputs: ["valueChange", "actionTriggered"] }, { kind: "component", type: UiFileInputComponent$1, selector: "forms-file-input", inputs: ["label", "hint", "accept", "multiple", "disabled", "value"], outputs: ["valueChange", "filesChange"] }, { kind: "component", type: UiPhotoInputComponent$1, selector: "forms-photo-input", inputs: ["label", "hint", "accept", "multiple", "limit", "disabled", "value", "uploadPhoto"], outputs: ["valueChange", "filesChange", "uploadingChange"] }, { kind: "component", type: UiCheckboxInputComponent$1, selector: "forms-checkbox-input", inputs: ["label", "disabled", "value"], outputs: ["valueChange"] }, { kind: "component", type: UiColorInputComponent$1, selector: "forms-color-input", inputs: ["label", "hint", "name", "id", "required", "fullWidth", "error"] }, { kind: "component", type: UiSlugInputComponent$1, selector: "forms-slug-input", inputs: ["label", "hint", "placeholder", "name", "id", "required", "fullWidth", "autocomplete", "readonly", "error"], outputs: ["invalidInput"] }, { kind: "component", type: UiBarcodeScannerInputComponent$1, 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$1, 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"] }] });
|
|
532
|
+
}
|
|
533
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDynamicFormComponent, decorators: [{
|
|
534
|
+
type: Component,
|
|
535
|
+
args: [{ selector: 'forms-dynamic-form', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, UiFormInputHostComponent$1, UiFileInputComponent$1, UiPhotoInputComponent$1, UiCheckboxInputComponent$1, UiColorInputComponent$1, UiSlugInputComponent$1, UiBarcodeScannerInputComponent$1, UiSelectComponent$1], providers: [UiFormContext$1, 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 [uploadPhoto]=\"uploadPhoto\"\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" }]
|
|
536
|
+
}], propDecorators: { fields: [{
|
|
537
|
+
type: Input
|
|
538
|
+
}], value: [{
|
|
539
|
+
type: Input
|
|
540
|
+
}], errors: [{
|
|
541
|
+
type: Input
|
|
542
|
+
}], submitLabel: [{
|
|
543
|
+
type: Input
|
|
544
|
+
}], showSubmit: [{
|
|
545
|
+
type: Input
|
|
546
|
+
}], disabled: [{
|
|
547
|
+
type: Input
|
|
548
|
+
}], uploadPhoto: [{
|
|
549
|
+
type: Input
|
|
550
|
+
}], valueChange: [{
|
|
551
|
+
type: Output
|
|
552
|
+
}], lookupCompleted: [{
|
|
553
|
+
type: Output
|
|
554
|
+
}], submitted: [{
|
|
555
|
+
type: Output
|
|
556
|
+
}], filesChange: [{
|
|
557
|
+
type: Output
|
|
558
|
+
}], uploadingChange: [{
|
|
559
|
+
type: Output
|
|
560
|
+
}], fieldActionTriggered: [{
|
|
561
|
+
type: Output
|
|
562
|
+
}] } });
|
|
563
|
+
|
|
564
|
+
class UiActionFormModalComponent {
|
|
565
|
+
formId = input('', ...(ngDevMode ? [{ debugName: "formId" }] : /* istanbul ignore next */ []));
|
|
566
|
+
recordId = input('', ...(ngDevMode ? [{ debugName: "recordId" }] : /* istanbul ignore next */ []));
|
|
567
|
+
isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
568
|
+
initialValue = input({}, ...(ngDevMode ? [{ debugName: "initialValue" }] : /* istanbul ignore next */ []));
|
|
569
|
+
uploadPhoto = input(null, ...(ngDevMode ? [{ debugName: "uploadPhoto" }] : /* istanbul ignore next */ []));
|
|
570
|
+
closed = output();
|
|
571
|
+
completed = output();
|
|
572
|
+
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
573
|
+
isSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitting" }] : /* istanbul ignore next */ []));
|
|
574
|
+
formConfig = signal(null, ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
|
|
575
|
+
formValue = signal({}, ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
576
|
+
fields = computed(() => this.formConfig()?.fields ?? [], ...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
|
|
577
|
+
submitAction = computed(() => this.formConfig()?.actions?.find((action) => action.type === 'submit' || action.type === 'api'), ...(ngDevMode ? [{ debugName: "submitAction" }] : /* istanbul ignore next */ []));
|
|
578
|
+
submitLabel = computed(() => this.submitAction()?.submit || this.submitAction()?.label || 'Save', ...(ngDevMode ? [{ debugName: "submitLabel" }] : /* istanbul ignore next */ []));
|
|
579
|
+
http = inject(HttpClient);
|
|
580
|
+
router = inject(Router);
|
|
581
|
+
ngOnChanges(changes) {
|
|
582
|
+
if (this.isOpen() && ('isOpen' in changes || 'formId' in changes || 'recordId' in changes)) {
|
|
583
|
+
this.loadForm();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
close() {
|
|
587
|
+
this.closed.emit();
|
|
588
|
+
}
|
|
589
|
+
onValueChange(value) {
|
|
590
|
+
this.formValue.set(value);
|
|
591
|
+
}
|
|
592
|
+
onFieldActionTriggered(event) {
|
|
593
|
+
const endpoint = this.resolveEndpoint(event.action.api);
|
|
594
|
+
if (!endpoint)
|
|
595
|
+
return;
|
|
596
|
+
this.http.request(this.resolveMethod(event.action.method), endpoint, { body: { ...this.formValue(), ...(event.action.payload || {}) } }).subscribe({
|
|
597
|
+
next: (response) => {
|
|
598
|
+
const data = this.extractData(response);
|
|
599
|
+
if (data)
|
|
600
|
+
this.formValue.update((current) => ({ ...current, ...data }));
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
confirm() {
|
|
605
|
+
if (this.isSubmitting())
|
|
606
|
+
return;
|
|
607
|
+
const action = this.submitAction();
|
|
608
|
+
const endpoint = this.resolveEndpoint(this.formConfig()?.api || action?.api);
|
|
609
|
+
if (!endpoint)
|
|
610
|
+
return;
|
|
611
|
+
const payload = { ...(this.initialValue() || {}), ...(action?.payload || {}), ...this.formValue() };
|
|
612
|
+
const recordId = String(this.recordId() || '').trim();
|
|
613
|
+
if (recordId && !Object.prototype.hasOwnProperty.call(payload, 'id'))
|
|
614
|
+
payload['id'] = recordId;
|
|
615
|
+
this.isSubmitting.set(true);
|
|
616
|
+
this.http.request(this.resolveMethod(action?.method || (recordId ? 'PATCH' : 'POST')), endpoint, { body: payload }).subscribe({
|
|
617
|
+
next: (response) => {
|
|
618
|
+
this.isSubmitting.set(false);
|
|
619
|
+
this.completed.emit(response);
|
|
620
|
+
this.navigateAfterSubmit(response);
|
|
621
|
+
this.close();
|
|
622
|
+
},
|
|
623
|
+
error: () => this.isSubmitting.set(false),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
loadForm() {
|
|
627
|
+
const formId = String(this.formId() || '').trim();
|
|
628
|
+
if (!formId)
|
|
629
|
+
return;
|
|
630
|
+
this.isLoading.set(true);
|
|
631
|
+
const recordId = String(this.recordId() || '').trim();
|
|
632
|
+
const endpoint = `/api/crud/form/${encodeURIComponent(formId)}${recordId ? `/${encodeURIComponent(recordId)}` : ''}`;
|
|
633
|
+
this.http.get(endpoint).subscribe({
|
|
634
|
+
next: (response) => {
|
|
635
|
+
const config = response.form || response;
|
|
636
|
+
this.formConfig.set(config);
|
|
637
|
+
this.formValue.set({ ...(config.defaults || {}), ...(config.item || {}), ...(this.initialValue() || {}), ...(recordId ? { id: recordId } : {}) });
|
|
638
|
+
this.isLoading.set(false);
|
|
639
|
+
},
|
|
640
|
+
error: () => {
|
|
641
|
+
this.formConfig.set(null);
|
|
642
|
+
this.isLoading.set(false);
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
resolveEndpoint(endpoint) {
|
|
647
|
+
const value = String(endpoint || '').trim();
|
|
648
|
+
if (!value)
|
|
649
|
+
return '';
|
|
650
|
+
if (/^https?:\/\//i.test(value) || value.startsWith('/api/'))
|
|
651
|
+
return value;
|
|
652
|
+
return `/api${value.startsWith('/') ? value : `/${value}`}`;
|
|
653
|
+
}
|
|
654
|
+
resolveMethod(method) {
|
|
655
|
+
return String(method || 'POST').toUpperCase();
|
|
656
|
+
}
|
|
657
|
+
extractData(response) {
|
|
658
|
+
if (!response || typeof response !== 'object')
|
|
659
|
+
return null;
|
|
660
|
+
const value = response.data;
|
|
661
|
+
return value && typeof value === 'object' ? value : response;
|
|
662
|
+
}
|
|
663
|
+
navigateAfterSubmit(response) {
|
|
664
|
+
const redirect = this.formConfig()?.redirectTo || this.submitAction()?.redirectTo;
|
|
665
|
+
if (!redirect)
|
|
666
|
+
return;
|
|
667
|
+
const data = this.extractData(response) || {};
|
|
668
|
+
const target = redirect.replace(/:([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, key) => {
|
|
669
|
+
const value = String(key).split('.').reduce((current, part) => current && typeof current === 'object' ? current[part] : undefined, data);
|
|
670
|
+
return value == null ? '' : String(value);
|
|
671
|
+
});
|
|
672
|
+
if (target && !target.includes(':'))
|
|
673
|
+
void this.router.navigateByUrl(target.startsWith('/') ? target : `/${target}`);
|
|
674
|
+
}
|
|
675
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiActionFormModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
676
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiActionFormModalComponent, isStandalone: true, selector: "forms-action-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 }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, uploadPhoto: { classPropertyName: "uploadPhoto", publicName: "uploadPhoto", 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 items-center justify-center bg-slate-950/40 p-4\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-slate-50 shadow-2xl\">\n <div class=\"flex items-center justify-between border-b border-slate-200 bg-white px-6 py-4\">\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=\"p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto()\" (valueChange)=\"onValueChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"flex justify-end gap-3 border-t border-slate-200 bg-white px-6 py-4\">\n <ui-button variant=\"ghost\" (click)=\"close()\">Cancel</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiDynamicFormComponent$1, selector: "forms-dynamic-form", inputs: ["fields", "value", "errors", "submitLabel", "showSubmit", "disabled", "uploadPhoto"], outputs: ["valueChange", "lookupCompleted", "submitted", "filesChange", "uploadingChange", "fieldActionTriggered"] }] });
|
|
677
|
+
}
|
|
678
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiActionFormModalComponent, decorators: [{
|
|
679
|
+
type: Component,
|
|
680
|
+
args: [{ selector: 'forms-action-form-modal', standalone: true, imports: [CommonModule, UiButtonComponent, UiDynamicFormComponent$1], template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 p-4\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-slate-50 shadow-2xl\">\n <div class=\"flex items-center justify-between border-b border-slate-200 bg-white px-6 py-4\">\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=\"p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto()\" (valueChange)=\"onValueChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"flex justify-end gap-3 border-t border-slate-200 bg-white px-6 py-4\">\n <ui-button variant=\"ghost\" (click)=\"close()\">Cancel</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n" }]
|
|
681
|
+
}], propDecorators: { formId: [{ type: i0.Input, args: [{ isSignal: true, alias: "formId", required: false }] }], recordId: [{ type: i0.Input, args: [{ isSignal: true, alias: "recordId", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], uploadPhoto: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadPhoto", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], completed: [{ type: i0.Output, args: ["completed"] }] } });
|
|
682
|
+
|
|
683
|
+
class UiDateInputComponent {
|
|
684
|
+
cdr = inject(ChangeDetectorRef);
|
|
685
|
+
label;
|
|
686
|
+
hint;
|
|
687
|
+
placeholder;
|
|
688
|
+
name;
|
|
689
|
+
id;
|
|
690
|
+
type = 'date';
|
|
691
|
+
element = 'input';
|
|
692
|
+
rows = 4;
|
|
693
|
+
required = false;
|
|
694
|
+
fullWidth = false;
|
|
695
|
+
autocomplete;
|
|
696
|
+
readonly = false;
|
|
697
|
+
pattern;
|
|
698
|
+
error;
|
|
699
|
+
actions = [];
|
|
700
|
+
disabled = false;
|
|
701
|
+
value = '';
|
|
702
|
+
static nextId = 0;
|
|
703
|
+
generatedId = `ui-input-${UiDateInputComponent.nextId++}`;
|
|
704
|
+
actionTriggered = new EventEmitter();
|
|
705
|
+
get controlId() {
|
|
706
|
+
return this.id ?? this.generatedId;
|
|
707
|
+
}
|
|
708
|
+
onChange = () => undefined;
|
|
709
|
+
onTouched = () => undefined;
|
|
710
|
+
writeValue(value) {
|
|
711
|
+
this.value = value ?? '';
|
|
712
|
+
this.cdr.markForCheck();
|
|
713
|
+
}
|
|
714
|
+
registerOnChange(fn) {
|
|
715
|
+
this.onChange = fn;
|
|
716
|
+
}
|
|
717
|
+
registerOnTouched(fn) {
|
|
718
|
+
this.onTouched = fn;
|
|
719
|
+
}
|
|
720
|
+
setDisabledState(isDisabled) {
|
|
721
|
+
this.disabled = isDisabled;
|
|
722
|
+
this.cdr.markForCheck();
|
|
723
|
+
}
|
|
724
|
+
onValueChange(value) {
|
|
725
|
+
const parsed = value;
|
|
726
|
+
this.value = parsed;
|
|
727
|
+
this.onChange(parsed);
|
|
728
|
+
}
|
|
729
|
+
triggerAction(action) {
|
|
730
|
+
this.actionTriggered.emit(action);
|
|
731
|
+
}
|
|
732
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
733
|
+
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" }, providers: [
|
|
734
|
+
{
|
|
735
|
+
provide: NG_VALUE_ACCESSOR,
|
|
736
|
+
useExisting: forwardRef(() => UiDateInputComponent),
|
|
737
|
+
multi: true,
|
|
738
|
+
},
|
|
739
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
740
|
+
}
|
|
741
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, decorators: [{
|
|
742
|
+
type: Component,
|
|
743
|
+
args: [{ selector: 'forms-date-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
744
|
+
{
|
|
745
|
+
provide: NG_VALUE_ACCESSOR,
|
|
746
|
+
useExisting: forwardRef(() => UiDateInputComponent),
|
|
747
|
+
multi: true,
|
|
748
|
+
},
|
|
749
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
750
|
+
}], propDecorators: { label: [{
|
|
751
|
+
type: Input
|
|
752
|
+
}], hint: [{
|
|
753
|
+
type: Input
|
|
754
|
+
}], placeholder: [{
|
|
755
|
+
type: Input
|
|
756
|
+
}], name: [{
|
|
757
|
+
type: Input
|
|
758
|
+
}], id: [{
|
|
759
|
+
type: Input
|
|
760
|
+
}], rows: [{
|
|
761
|
+
type: Input
|
|
762
|
+
}], required: [{
|
|
763
|
+
type: Input
|
|
764
|
+
}], fullWidth: [{
|
|
765
|
+
type: Input
|
|
766
|
+
}], autocomplete: [{
|
|
767
|
+
type: Input
|
|
768
|
+
}], readonly: [{
|
|
769
|
+
type: Input
|
|
770
|
+
}], pattern: [{
|
|
771
|
+
type: Input
|
|
772
|
+
}], error: [{
|
|
773
|
+
type: Input
|
|
774
|
+
}], actions: [{
|
|
775
|
+
type: Input
|
|
776
|
+
}], actionTriggered: [{
|
|
777
|
+
type: Output
|
|
778
|
+
}] } });
|
|
779
|
+
|
|
780
|
+
class UiDatetimeInputComponent {
|
|
781
|
+
cdr = inject(ChangeDetectorRef);
|
|
782
|
+
label;
|
|
783
|
+
hint;
|
|
784
|
+
placeholder;
|
|
785
|
+
name;
|
|
786
|
+
id;
|
|
787
|
+
type = 'datetime-local';
|
|
788
|
+
element = 'input';
|
|
789
|
+
rows = 4;
|
|
790
|
+
required = false;
|
|
791
|
+
fullWidth = false;
|
|
792
|
+
autocomplete;
|
|
793
|
+
readonly = false;
|
|
794
|
+
pattern;
|
|
795
|
+
error;
|
|
796
|
+
actions = [];
|
|
797
|
+
disabled = false;
|
|
798
|
+
value = '';
|
|
799
|
+
static nextId = 0;
|
|
800
|
+
generatedId = `ui-input-${UiDatetimeInputComponent.nextId++}`;
|
|
801
|
+
actionTriggered = new EventEmitter();
|
|
802
|
+
get controlId() {
|
|
803
|
+
return this.id ?? this.generatedId;
|
|
804
|
+
}
|
|
805
|
+
onChange = () => undefined;
|
|
806
|
+
onTouched = () => undefined;
|
|
807
|
+
writeValue(value) {
|
|
808
|
+
this.value = value ?? '';
|
|
809
|
+
this.cdr.markForCheck();
|
|
810
|
+
}
|
|
811
|
+
registerOnChange(fn) {
|
|
812
|
+
this.onChange = fn;
|
|
813
|
+
}
|
|
814
|
+
registerOnTouched(fn) {
|
|
815
|
+
this.onTouched = fn;
|
|
816
|
+
}
|
|
817
|
+
setDisabledState(isDisabled) {
|
|
818
|
+
this.disabled = isDisabled;
|
|
819
|
+
this.cdr.markForCheck();
|
|
820
|
+
}
|
|
821
|
+
onValueChange(value) {
|
|
822
|
+
const parsed = value;
|
|
823
|
+
this.value = parsed;
|
|
824
|
+
this.onChange(parsed);
|
|
825
|
+
}
|
|
826
|
+
triggerAction(action) {
|
|
827
|
+
this.actionTriggered.emit(action);
|
|
828
|
+
}
|
|
829
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDatetimeInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
830
|
+
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" }, providers: [
|
|
831
|
+
{
|
|
832
|
+
provide: NG_VALUE_ACCESSOR,
|
|
833
|
+
useExisting: forwardRef(() => UiDatetimeInputComponent),
|
|
834
|
+
multi: true,
|
|
835
|
+
},
|
|
836
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
837
|
+
}
|
|
838
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDatetimeInputComponent, decorators: [{
|
|
839
|
+
type: Component,
|
|
840
|
+
args: [{ selector: 'forms-datetime-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
841
|
+
{
|
|
842
|
+
provide: NG_VALUE_ACCESSOR,
|
|
843
|
+
useExisting: forwardRef(() => UiDatetimeInputComponent),
|
|
844
|
+
multi: true,
|
|
845
|
+
},
|
|
846
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
847
|
+
}], propDecorators: { label: [{
|
|
848
|
+
type: Input
|
|
849
|
+
}], hint: [{
|
|
850
|
+
type: Input
|
|
851
|
+
}], placeholder: [{
|
|
852
|
+
type: Input
|
|
853
|
+
}], name: [{
|
|
854
|
+
type: Input
|
|
855
|
+
}], id: [{
|
|
856
|
+
type: Input
|
|
857
|
+
}], rows: [{
|
|
858
|
+
type: Input
|
|
859
|
+
}], required: [{
|
|
860
|
+
type: Input
|
|
861
|
+
}], fullWidth: [{
|
|
862
|
+
type: Input
|
|
863
|
+
}], autocomplete: [{
|
|
864
|
+
type: Input
|
|
865
|
+
}], readonly: [{
|
|
866
|
+
type: Input
|
|
867
|
+
}], pattern: [{
|
|
868
|
+
type: Input
|
|
869
|
+
}], error: [{
|
|
870
|
+
type: Input
|
|
871
|
+
}], actions: [{
|
|
872
|
+
type: Input
|
|
873
|
+
}], actionTriggered: [{
|
|
874
|
+
type: Output
|
|
875
|
+
}] } });
|
|
876
|
+
|
|
877
|
+
class UiEmailInputComponent {
|
|
878
|
+
cdr = inject(ChangeDetectorRef);
|
|
879
|
+
label;
|
|
880
|
+
hint;
|
|
881
|
+
placeholder;
|
|
882
|
+
name;
|
|
883
|
+
id;
|
|
884
|
+
type = 'email';
|
|
885
|
+
element = 'input';
|
|
886
|
+
rows = 4;
|
|
887
|
+
required = false;
|
|
888
|
+
fullWidth = false;
|
|
889
|
+
autocomplete;
|
|
890
|
+
readonly = false;
|
|
891
|
+
pattern;
|
|
892
|
+
error;
|
|
893
|
+
actions = [];
|
|
894
|
+
disabled = false;
|
|
895
|
+
value = '';
|
|
896
|
+
static nextId = 0;
|
|
897
|
+
generatedId = `ui-input-${UiEmailInputComponent.nextId++}`;
|
|
898
|
+
actionTriggered = new EventEmitter();
|
|
899
|
+
get controlId() {
|
|
900
|
+
return this.id ?? this.generatedId;
|
|
901
|
+
}
|
|
902
|
+
onChange = () => undefined;
|
|
903
|
+
onTouched = () => undefined;
|
|
904
|
+
writeValue(value) {
|
|
905
|
+
this.value = value ?? '';
|
|
906
|
+
this.cdr.markForCheck();
|
|
907
|
+
}
|
|
908
|
+
registerOnChange(fn) {
|
|
909
|
+
this.onChange = fn;
|
|
910
|
+
}
|
|
911
|
+
registerOnTouched(fn) {
|
|
912
|
+
this.onTouched = fn;
|
|
913
|
+
}
|
|
914
|
+
setDisabledState(isDisabled) {
|
|
915
|
+
this.disabled = isDisabled;
|
|
916
|
+
this.cdr.markForCheck();
|
|
917
|
+
}
|
|
918
|
+
onValueChange(value) {
|
|
919
|
+
const parsed = value;
|
|
920
|
+
this.value = parsed;
|
|
921
|
+
this.onChange(parsed);
|
|
922
|
+
}
|
|
923
|
+
triggerAction(action) {
|
|
924
|
+
this.actionTriggered.emit(action);
|
|
925
|
+
}
|
|
926
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
927
|
+
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: [
|
|
928
|
+
{
|
|
929
|
+
provide: NG_VALUE_ACCESSOR,
|
|
930
|
+
useExisting: forwardRef(() => UiEmailInputComponent),
|
|
931
|
+
multi: true,
|
|
932
|
+
},
|
|
933
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
934
|
+
}
|
|
935
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, decorators: [{
|
|
936
|
+
type: Component,
|
|
937
|
+
args: [{ selector: 'forms-email-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
938
|
+
{
|
|
939
|
+
provide: NG_VALUE_ACCESSOR,
|
|
940
|
+
useExisting: forwardRef(() => UiEmailInputComponent),
|
|
941
|
+
multi: true,
|
|
942
|
+
},
|
|
943
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
944
|
+
}], propDecorators: { label: [{
|
|
945
|
+
type: Input
|
|
946
|
+
}], hint: [{
|
|
947
|
+
type: Input
|
|
948
|
+
}], placeholder: [{
|
|
949
|
+
type: Input
|
|
950
|
+
}], name: [{
|
|
951
|
+
type: Input
|
|
952
|
+
}], id: [{
|
|
953
|
+
type: Input
|
|
954
|
+
}], rows: [{
|
|
955
|
+
type: Input
|
|
956
|
+
}], required: [{
|
|
957
|
+
type: Input
|
|
958
|
+
}], fullWidth: [{
|
|
959
|
+
type: Input
|
|
960
|
+
}], autocomplete: [{
|
|
961
|
+
type: Input
|
|
962
|
+
}], readonly: [{
|
|
963
|
+
type: Input
|
|
964
|
+
}], pattern: [{
|
|
965
|
+
type: Input
|
|
966
|
+
}], error: [{
|
|
967
|
+
type: Input
|
|
968
|
+
}], actions: [{
|
|
969
|
+
type: Input
|
|
970
|
+
}], actionTriggered: [{
|
|
971
|
+
type: Output
|
|
972
|
+
}] } });
|
|
973
|
+
|
|
974
|
+
class UiNumberInputComponent {
|
|
975
|
+
cdr = inject(ChangeDetectorRef);
|
|
976
|
+
label;
|
|
977
|
+
hint;
|
|
978
|
+
placeholder;
|
|
979
|
+
name;
|
|
980
|
+
id;
|
|
981
|
+
type = 'number';
|
|
982
|
+
element = 'input';
|
|
983
|
+
rows = 4;
|
|
984
|
+
required = false;
|
|
985
|
+
fullWidth = false;
|
|
986
|
+
autocomplete;
|
|
987
|
+
readonly = false;
|
|
988
|
+
disabled = false;
|
|
989
|
+
pattern;
|
|
990
|
+
error;
|
|
991
|
+
actions = [];
|
|
992
|
+
value = '';
|
|
993
|
+
static nextId = 0;
|
|
994
|
+
generatedId = `ui-input-${UiNumberInputComponent.nextId++}`;
|
|
995
|
+
actionTriggered = new EventEmitter();
|
|
996
|
+
get controlId() {
|
|
997
|
+
return this.id ?? this.generatedId;
|
|
998
|
+
}
|
|
999
|
+
onChange = () => undefined;
|
|
1000
|
+
onTouched = () => undefined;
|
|
1001
|
+
writeValue(value) {
|
|
1002
|
+
this.value = value ?? '';
|
|
1003
|
+
this.cdr.markForCheck();
|
|
1004
|
+
}
|
|
1005
|
+
registerOnChange(fn) {
|
|
1006
|
+
this.onChange = fn;
|
|
1007
|
+
}
|
|
1008
|
+
registerOnTouched(fn) {
|
|
1009
|
+
this.onTouched = fn;
|
|
1010
|
+
}
|
|
1011
|
+
setDisabledState(isDisabled) {
|
|
1012
|
+
this.disabled = isDisabled;
|
|
1013
|
+
this.cdr.markForCheck();
|
|
1014
|
+
}
|
|
1015
|
+
onValueChange(value) {
|
|
1016
|
+
const parsed = value === '' ? null : Number(value);
|
|
1017
|
+
this.value = parsed;
|
|
1018
|
+
this.onChange(parsed);
|
|
1019
|
+
}
|
|
1020
|
+
triggerAction(action) {
|
|
1021
|
+
this.actionTriggered.emit(action);
|
|
1022
|
+
}
|
|
1023
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiNumberInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1024
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiNumberInputComponent, isStandalone: true, selector: "forms-number-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", disabled: "disabled", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
1025
|
+
{
|
|
1026
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1027
|
+
useExisting: forwardRef(() => UiNumberInputComponent),
|
|
1028
|
+
multi: true,
|
|
1029
|
+
},
|
|
1030
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
1031
|
+
}
|
|
1032
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiNumberInputComponent, decorators: [{
|
|
1033
|
+
type: Component,
|
|
1034
|
+
args: [{ selector: 'forms-number-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1035
|
+
{
|
|
1036
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1037
|
+
useExisting: forwardRef(() => UiNumberInputComponent),
|
|
1038
|
+
multi: true,
|
|
1039
|
+
},
|
|
1040
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
1041
|
+
}], propDecorators: { label: [{
|
|
1042
|
+
type: Input
|
|
1043
|
+
}], hint: [{
|
|
1044
|
+
type: Input
|
|
1045
|
+
}], placeholder: [{
|
|
1046
|
+
type: Input
|
|
1047
|
+
}], name: [{
|
|
1048
|
+
type: Input
|
|
1049
|
+
}], id: [{
|
|
1050
|
+
type: Input
|
|
1051
|
+
}], rows: [{
|
|
1052
|
+
type: Input
|
|
1053
|
+
}], required: [{
|
|
1054
|
+
type: Input
|
|
1055
|
+
}], fullWidth: [{
|
|
1056
|
+
type: Input
|
|
1057
|
+
}], autocomplete: [{
|
|
1058
|
+
type: Input
|
|
1059
|
+
}], readonly: [{
|
|
1060
|
+
type: Input
|
|
1061
|
+
}], disabled: [{
|
|
1062
|
+
type: Input
|
|
1063
|
+
}], pattern: [{
|
|
1064
|
+
type: Input
|
|
1065
|
+
}], error: [{
|
|
1066
|
+
type: Input
|
|
1067
|
+
}], actions: [{
|
|
1068
|
+
type: Input
|
|
1069
|
+
}], actionTriggered: [{
|
|
1070
|
+
type: Output
|
|
1071
|
+
}] } });
|
|
1072
|
+
|
|
1073
|
+
class UiPasswordInputComponent {
|
|
1074
|
+
cdr = inject(ChangeDetectorRef);
|
|
1075
|
+
label;
|
|
1076
|
+
hint;
|
|
1077
|
+
placeholder;
|
|
1078
|
+
name;
|
|
1079
|
+
id;
|
|
1080
|
+
type = 'password';
|
|
1081
|
+
element = 'input';
|
|
1082
|
+
rows = 4;
|
|
1083
|
+
required = false;
|
|
1084
|
+
fullWidth = false;
|
|
1085
|
+
autocomplete;
|
|
1086
|
+
readonly = false;
|
|
1087
|
+
pattern;
|
|
1088
|
+
error;
|
|
1089
|
+
actions = [];
|
|
1090
|
+
disabled = false;
|
|
1091
|
+
value = '';
|
|
1092
|
+
static nextId = 0;
|
|
1093
|
+
generatedId = `ui-input-${UiPasswordInputComponent.nextId++}`;
|
|
1094
|
+
actionTriggered = new EventEmitter();
|
|
1095
|
+
get controlId() {
|
|
1096
|
+
return this.id ?? this.generatedId;
|
|
1097
|
+
}
|
|
1098
|
+
onChange = () => undefined;
|
|
1099
|
+
onTouched = () => undefined;
|
|
1100
|
+
writeValue(value) {
|
|
1101
|
+
this.value = value ?? '';
|
|
1102
|
+
this.cdr.markForCheck();
|
|
1103
|
+
}
|
|
1104
|
+
registerOnChange(fn) {
|
|
1105
|
+
this.onChange = fn;
|
|
1106
|
+
}
|
|
1107
|
+
registerOnTouched(fn) {
|
|
1108
|
+
this.onTouched = fn;
|
|
1109
|
+
}
|
|
1110
|
+
setDisabledState(isDisabled) {
|
|
1111
|
+
this.disabled = isDisabled;
|
|
1112
|
+
this.cdr.markForCheck();
|
|
1113
|
+
}
|
|
1114
|
+
onValueChange(value) {
|
|
1115
|
+
const parsed = value;
|
|
1116
|
+
this.value = parsed;
|
|
1117
|
+
this.onChange(parsed);
|
|
1118
|
+
}
|
|
1119
|
+
triggerAction(action) {
|
|
1120
|
+
this.actionTriggered.emit(action);
|
|
1121
|
+
}
|
|
1122
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPasswordInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1123
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPasswordInputComponent, isStandalone: true, selector: "forms-password-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: [
|
|
1124
|
+
{
|
|
1125
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1126
|
+
useExisting: forwardRef(() => UiPasswordInputComponent),
|
|
1127
|
+
multi: true,
|
|
1128
|
+
},
|
|
1129
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
1130
|
+
}
|
|
1131
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPasswordInputComponent, decorators: [{
|
|
1132
|
+
type: Component,
|
|
1133
|
+
args: [{ selector: 'forms-password-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1134
|
+
{
|
|
1135
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1136
|
+
useExisting: forwardRef(() => UiPasswordInputComponent),
|
|
1137
|
+
multi: true,
|
|
1138
|
+
},
|
|
1139
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
1140
|
+
}], propDecorators: { label: [{
|
|
1141
|
+
type: Input
|
|
1142
|
+
}], hint: [{
|
|
1143
|
+
type: Input
|
|
1144
|
+
}], placeholder: [{
|
|
1145
|
+
type: Input
|
|
1146
|
+
}], name: [{
|
|
1147
|
+
type: Input
|
|
1148
|
+
}], id: [{
|
|
1149
|
+
type: Input
|
|
1150
|
+
}], rows: [{
|
|
1151
|
+
type: Input
|
|
1152
|
+
}], required: [{
|
|
1153
|
+
type: Input
|
|
1154
|
+
}], fullWidth: [{
|
|
1155
|
+
type: Input
|
|
1156
|
+
}], autocomplete: [{
|
|
1157
|
+
type: Input
|
|
1158
|
+
}], readonly: [{
|
|
1159
|
+
type: Input
|
|
1160
|
+
}], pattern: [{
|
|
1161
|
+
type: Input
|
|
1162
|
+
}], error: [{
|
|
1163
|
+
type: Input
|
|
1164
|
+
}], actions: [{
|
|
1165
|
+
type: Input
|
|
1166
|
+
}], actionTriggered: [{
|
|
1167
|
+
type: Output
|
|
1168
|
+
}] } });
|
|
1169
|
+
|
|
1170
|
+
class UiTextInputComponent {
|
|
1171
|
+
cdr = inject(ChangeDetectorRef);
|
|
1172
|
+
label;
|
|
1173
|
+
hint;
|
|
1174
|
+
placeholder;
|
|
1175
|
+
name;
|
|
1176
|
+
id;
|
|
1177
|
+
type = 'text';
|
|
1178
|
+
element = 'input';
|
|
1179
|
+
rows = 4;
|
|
1180
|
+
required = false;
|
|
1181
|
+
fullWidth = false;
|
|
1182
|
+
autocomplete;
|
|
1183
|
+
readonly = false;
|
|
1184
|
+
pattern;
|
|
1185
|
+
error;
|
|
1186
|
+
actions = [];
|
|
1187
|
+
disabled = false;
|
|
1188
|
+
value = '';
|
|
1189
|
+
static nextId = 0;
|
|
1190
|
+
generatedId = `ui-input-${UiTextInputComponent.nextId++}`;
|
|
1191
|
+
actionTriggered = new EventEmitter();
|
|
1192
|
+
get controlId() {
|
|
1193
|
+
return this.id ?? this.generatedId;
|
|
1194
|
+
}
|
|
1195
|
+
onChange = () => undefined;
|
|
1196
|
+
onTouched = () => undefined;
|
|
1197
|
+
writeValue(value) {
|
|
1198
|
+
this.value = value ?? '';
|
|
1199
|
+
this.cdr.markForCheck();
|
|
1200
|
+
}
|
|
1201
|
+
registerOnChange(fn) {
|
|
1202
|
+
this.onChange = fn;
|
|
1203
|
+
}
|
|
1204
|
+
registerOnTouched(fn) {
|
|
1205
|
+
this.onTouched = fn;
|
|
1206
|
+
}
|
|
1207
|
+
setDisabledState(isDisabled) {
|
|
1208
|
+
this.disabled = isDisabled;
|
|
1209
|
+
this.cdr.markForCheck();
|
|
1210
|
+
}
|
|
1211
|
+
onValueChange(value) {
|
|
1212
|
+
const parsed = value;
|
|
1213
|
+
this.value = parsed;
|
|
1214
|
+
this.onChange(parsed);
|
|
1215
|
+
}
|
|
1216
|
+
triggerAction(action) {
|
|
1217
|
+
this.actionTriggered.emit(action);
|
|
1218
|
+
}
|
|
1219
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1220
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiTextInputComponent, isStandalone: true, selector: "forms-text-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: [
|
|
1221
|
+
{
|
|
1222
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1223
|
+
useExisting: forwardRef(() => UiTextInputComponent),
|
|
1224
|
+
multi: true,
|
|
1225
|
+
},
|
|
1226
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
1227
|
+
}
|
|
1228
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextInputComponent, decorators: [{
|
|
1229
|
+
type: Component,
|
|
1230
|
+
args: [{ selector: 'forms-text-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1231
|
+
{
|
|
1232
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1233
|
+
useExisting: forwardRef(() => UiTextInputComponent),
|
|
1234
|
+
multi: true,
|
|
1235
|
+
},
|
|
1236
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
1237
|
+
}], propDecorators: { label: [{
|
|
1238
|
+
type: Input
|
|
1239
|
+
}], hint: [{
|
|
1240
|
+
type: Input
|
|
1241
|
+
}], placeholder: [{
|
|
1242
|
+
type: Input
|
|
1243
|
+
}], name: [{
|
|
1244
|
+
type: Input
|
|
1245
|
+
}], id: [{
|
|
1246
|
+
type: Input
|
|
1247
|
+
}], rows: [{
|
|
1248
|
+
type: Input
|
|
1249
|
+
}], required: [{
|
|
1250
|
+
type: Input
|
|
1251
|
+
}], fullWidth: [{
|
|
1252
|
+
type: Input
|
|
1253
|
+
}], autocomplete: [{
|
|
1254
|
+
type: Input
|
|
1255
|
+
}], readonly: [{
|
|
1256
|
+
type: Input
|
|
1257
|
+
}], pattern: [{
|
|
1258
|
+
type: Input
|
|
1259
|
+
}], error: [{
|
|
1260
|
+
type: Input
|
|
1261
|
+
}], actions: [{
|
|
1262
|
+
type: Input
|
|
1263
|
+
}], actionTriggered: [{
|
|
1264
|
+
type: Output
|
|
1265
|
+
}] } });
|
|
1266
|
+
|
|
1267
|
+
class UiTextareaInputComponent {
|
|
1268
|
+
cdr = inject(ChangeDetectorRef);
|
|
1269
|
+
label;
|
|
1270
|
+
hint;
|
|
1271
|
+
placeholder;
|
|
1272
|
+
name;
|
|
1273
|
+
id;
|
|
1274
|
+
type = 'text';
|
|
1275
|
+
element = 'textarea';
|
|
1276
|
+
rows = 4;
|
|
1277
|
+
required = false;
|
|
1278
|
+
fullWidth = false;
|
|
1279
|
+
autocomplete;
|
|
1280
|
+
readonly = false;
|
|
1281
|
+
pattern;
|
|
1282
|
+
error;
|
|
1283
|
+
actions = [];
|
|
1284
|
+
disabled = false;
|
|
1285
|
+
value = '';
|
|
1286
|
+
static nextId = 0;
|
|
1287
|
+
generatedId = `ui-input-${UiTextareaInputComponent.nextId++}`;
|
|
1288
|
+
actionTriggered = new EventEmitter();
|
|
1289
|
+
get controlId() {
|
|
1290
|
+
return this.id ?? this.generatedId;
|
|
1291
|
+
}
|
|
1292
|
+
onChange = () => undefined;
|
|
1293
|
+
onTouched = () => undefined;
|
|
1294
|
+
writeValue(value) {
|
|
1295
|
+
this.value = value ?? '';
|
|
1296
|
+
this.cdr.markForCheck();
|
|
1297
|
+
}
|
|
1298
|
+
registerOnChange(fn) {
|
|
1299
|
+
this.onChange = fn;
|
|
1300
|
+
}
|
|
1301
|
+
registerOnTouched(fn) {
|
|
1302
|
+
this.onTouched = fn;
|
|
1303
|
+
}
|
|
1304
|
+
setDisabledState(isDisabled) {
|
|
1305
|
+
this.disabled = isDisabled;
|
|
1306
|
+
this.cdr.markForCheck();
|
|
1307
|
+
}
|
|
1308
|
+
onValueChange(value) {
|
|
1309
|
+
const parsed = value;
|
|
1310
|
+
this.value = parsed;
|
|
1311
|
+
this.onChange(parsed);
|
|
1312
|
+
}
|
|
1313
|
+
triggerAction(action) {
|
|
1314
|
+
this.actionTriggered.emit(action);
|
|
1315
|
+
}
|
|
1316
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextareaInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1317
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiTextareaInputComponent, isStandalone: true, selector: "forms-textarea-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: [
|
|
1318
|
+
{
|
|
1319
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1320
|
+
useExisting: forwardRef(() => UiTextareaInputComponent),
|
|
1321
|
+
multi: true,
|
|
1322
|
+
},
|
|
1323
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
1324
|
+
}
|
|
1325
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextareaInputComponent, decorators: [{
|
|
1326
|
+
type: Component,
|
|
1327
|
+
args: [{ selector: 'forms-textarea-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
1328
|
+
{
|
|
1329
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1330
|
+
useExisting: forwardRef(() => UiTextareaInputComponent),
|
|
1331
|
+
multi: true,
|
|
1332
|
+
},
|
|
1333
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
1334
|
+
}], propDecorators: { label: [{
|
|
1335
|
+
type: Input
|
|
1336
|
+
}], hint: [{
|
|
1337
|
+
type: Input
|
|
1338
|
+
}], placeholder: [{
|
|
1339
|
+
type: Input
|
|
1340
|
+
}], name: [{
|
|
1341
|
+
type: Input
|
|
1342
|
+
}], id: [{
|
|
1343
|
+
type: Input
|
|
1344
|
+
}], rows: [{
|
|
1345
|
+
type: Input
|
|
1346
|
+
}], required: [{
|
|
1347
|
+
type: Input
|
|
1348
|
+
}], fullWidth: [{
|
|
1349
|
+
type: Input
|
|
1350
|
+
}], autocomplete: [{
|
|
1351
|
+
type: Input
|
|
1352
|
+
}], readonly: [{
|
|
1353
|
+
type: Input
|
|
1354
|
+
}], pattern: [{
|
|
1355
|
+
type: Input
|
|
1356
|
+
}], error: [{
|
|
1357
|
+
type: Input
|
|
1358
|
+
}], actions: [{
|
|
1359
|
+
type: Input
|
|
1360
|
+
}], actionTriggered: [{
|
|
1361
|
+
type: Output
|
|
1362
|
+
}] } });
|
|
1363
|
+
|
|
1364
|
+
const UI_FORM_INPUT_REGISTRY = {
|
|
1365
|
+
text: UiTextInputComponent,
|
|
1366
|
+
number: UiNumberInputComponent,
|
|
1367
|
+
date: UiDateInputComponent,
|
|
1368
|
+
datetime: UiDatetimeInputComponent,
|
|
1369
|
+
email: UiEmailInputComponent,
|
|
1370
|
+
password: UiPasswordInputComponent,
|
|
1371
|
+
textarea: UiTextareaInputComponent,
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
class UiFormInputHostComponent {
|
|
1375
|
+
field;
|
|
1376
|
+
value;
|
|
1377
|
+
error = '';
|
|
1378
|
+
disabled = false;
|
|
1379
|
+
actions = [];
|
|
1380
|
+
options = [];
|
|
1381
|
+
valueChange = new EventEmitter();
|
|
1382
|
+
actionTriggered = new EventEmitter();
|
|
1383
|
+
viewContainer = inject(ViewContainerRef);
|
|
1384
|
+
componentRef;
|
|
1385
|
+
ngOnChanges(changes) {
|
|
1386
|
+
if (changes['field']) {
|
|
1387
|
+
this.render();
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
this.syncInputs();
|
|
1391
|
+
}
|
|
1392
|
+
render() {
|
|
1393
|
+
this.viewContainer.clear();
|
|
1394
|
+
const inputType = String(this.field?.type || 'text');
|
|
1395
|
+
const componentType = UI_FORM_INPUT_REGISTRY[inputType] || UI_FORM_INPUT_REGISTRY.text;
|
|
1396
|
+
this.componentRef = this.viewContainer.createComponent(componentType);
|
|
1397
|
+
this.syncInputs();
|
|
1398
|
+
const instance = this.componentRef.instance;
|
|
1399
|
+
instance.registerOnChange?.((nextValue) => this.valueChange.emit(nextValue));
|
|
1400
|
+
instance.registerOnTouched?.(() => undefined);
|
|
1401
|
+
instance.actionTriggered?.subscribe((action) => this.actionTriggered.emit(action));
|
|
1402
|
+
}
|
|
1403
|
+
syncInputs() {
|
|
1404
|
+
if (!this.componentRef || !this.field)
|
|
1405
|
+
return;
|
|
1406
|
+
const field = this.field;
|
|
1407
|
+
const setInput = (name, value) => this.componentRef?.setInput(name, value);
|
|
1408
|
+
setInput('name', field.id);
|
|
1409
|
+
setInput('id', field.id);
|
|
1410
|
+
setInput('label', field.label);
|
|
1411
|
+
setInput('placeholder', field.placeholder);
|
|
1412
|
+
setInput('hint', field.hint);
|
|
1413
|
+
setInput('required', !!field.required);
|
|
1414
|
+
setInput('readonly', !!field.readonly);
|
|
1415
|
+
setInput('disabled', this.disabled || !!field.disabled);
|
|
1416
|
+
setInput('pattern', field.pattern);
|
|
1417
|
+
setInput('rows', field.rows ?? 4);
|
|
1418
|
+
setInput('fullWidth', field.fullWidth !== false);
|
|
1419
|
+
setInput('error', this.error);
|
|
1420
|
+
setInput('actions', this.actions);
|
|
1421
|
+
const instance = this.componentRef.instance;
|
|
1422
|
+
instance.writeValue?.(this.value);
|
|
1423
|
+
instance.setDisabledState?.(this.disabled || !!field.disabled);
|
|
1424
|
+
}
|
|
1425
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1426
|
+
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 });
|
|
1427
|
+
}
|
|
1428
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, decorators: [{
|
|
1429
|
+
type: Component,
|
|
1430
|
+
args: [{
|
|
1431
|
+
selector: 'forms-form-input-host',
|
|
1432
|
+
standalone: true,
|
|
1433
|
+
template: '',
|
|
1434
|
+
}]
|
|
1435
|
+
}], propDecorators: { field: [{
|
|
1436
|
+
type: Input
|
|
1437
|
+
}], value: [{
|
|
1438
|
+
type: Input
|
|
1439
|
+
}], error: [{
|
|
1440
|
+
type: Input
|
|
1441
|
+
}], disabled: [{
|
|
1442
|
+
type: Input
|
|
1443
|
+
}], actions: [{
|
|
1444
|
+
type: Input
|
|
1445
|
+
}], options: [{
|
|
1446
|
+
type: Input
|
|
1447
|
+
}], valueChange: [{
|
|
1448
|
+
type: Output
|
|
1449
|
+
}], actionTriggered: [{
|
|
1450
|
+
type: Output
|
|
1451
|
+
}] } });
|
|
1452
|
+
|
|
1453
|
+
class UiBarcodeScannerInputComponent {
|
|
1454
|
+
value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1455
|
+
label = input('Штрихкод', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1456
|
+
placeholder = input('Відскануйте або введіть штрихкод', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
1457
|
+
hint = input('Сканер може просто ввести код і натиснути Enter.', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
1458
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1459
|
+
autoOpen = input(true, ...(ngDevMode ? [{ debugName: "autoOpen" }] : /* istanbul ignore next */ []));
|
|
1460
|
+
searchButtonLabel = input('Знайти', ...(ngDevMode ? [{ debugName: "searchButtonLabel" }] : /* istanbul ignore next */ []));
|
|
1461
|
+
openCameraLabel = input('Відкрити камеру', ...(ngDevMode ? [{ debugName: "openCameraLabel" }] : /* istanbul ignore next */ []));
|
|
1462
|
+
closeCameraLabel = input('Закрити камеру', ...(ngDevMode ? [{ debugName: "closeCameraLabel" }] : /* istanbul ignore next */ []));
|
|
1463
|
+
cameraTitle = input('Камера', ...(ngDevMode ? [{ debugName: "cameraTitle" }] : /* istanbul ignore next */ []));
|
|
1464
|
+
cameraStartingLabel = input('Запускаємо камеру…', ...(ngDevMode ? [{ debugName: "cameraStartingLabel" }] : /* istanbul ignore next */ []));
|
|
1465
|
+
cameraHint = input('Наведіть камеру на штрихкод. Після зчитування код підставиться автоматично.', ...(ngDevMode ? [{ debugName: "cameraHint" }] : /* istanbul ignore next */ []));
|
|
1466
|
+
valueChange = output();
|
|
1467
|
+
scanned = output();
|
|
1468
|
+
searchRequested = output();
|
|
1469
|
+
cameraStateChange = output();
|
|
1470
|
+
isCameraOpen = signal(false, ...(ngDevMode ? [{ debugName: "isCameraOpen" }] : /* istanbul ignore next */ []));
|
|
1471
|
+
isCameraStarting = signal(false, ...(ngDevMode ? [{ debugName: "isCameraStarting" }] : /* istanbul ignore next */ []));
|
|
1472
|
+
cameraError = signal('', ...(ngDevMode ? [{ debugName: "cameraError" }] : /* istanbul ignore next */ []));
|
|
1473
|
+
cameraStatus = signal('Камеру не відкрито', ...(ngDevMode ? [{ debugName: "cameraStatus" }] : /* istanbul ignore next */ []));
|
|
1474
|
+
cameraVideo;
|
|
1475
|
+
barcodeReader = new BrowserMultiFormatOneDReader(new Map([[DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.CODE_128]]]), {
|
|
1476
|
+
delayBetweenScanAttempts: 75,
|
|
1477
|
+
delayBetweenScanSuccess: 250,
|
|
1478
|
+
tryPlayVideoTimeout: 5000,
|
|
1479
|
+
});
|
|
1480
|
+
nativeBarcodeDetector = null;
|
|
1481
|
+
cameraControls = null;
|
|
1482
|
+
cameraStream = null;
|
|
1483
|
+
scanLoopId = null;
|
|
1484
|
+
autoOpenQueued = false;
|
|
1485
|
+
openToken = 0;
|
|
1486
|
+
ngOnChanges(changes) {
|
|
1487
|
+
if (changes['disabled']?.currentValue === true) {
|
|
1488
|
+
this.stopCamera();
|
|
1489
|
+
this.autoOpenQueued = false;
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (changes['autoOpen'] && this.autoOpen() && !this.disabled()) {
|
|
1493
|
+
this.scheduleAutoOpen();
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
ngAfterViewInit() {
|
|
1497
|
+
this.scheduleAutoOpen();
|
|
1498
|
+
}
|
|
1499
|
+
ngOnDestroy() {
|
|
1500
|
+
this.stopCamera();
|
|
1501
|
+
}
|
|
1502
|
+
onValueChange(nextValue) {
|
|
1503
|
+
this.valueChange.emit(String(nextValue ?? ''));
|
|
1504
|
+
}
|
|
1505
|
+
requestSearch() {
|
|
1506
|
+
this.searchRequested.emit();
|
|
1507
|
+
}
|
|
1508
|
+
async openCamera() {
|
|
1509
|
+
if (this.disabled() || this.isCameraStarting() || this.isCameraOpen()) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
this.cameraError.set('');
|
|
1513
|
+
this.cameraStatus.set('Запускаємо камеру…');
|
|
1514
|
+
this.isCameraStarting.set(true);
|
|
1515
|
+
this.cameraStateChange.emit('starting');
|
|
1516
|
+
this.isCameraOpen.set(true);
|
|
1517
|
+
const token = ++this.openToken;
|
|
1518
|
+
try {
|
|
1519
|
+
await new Promise((resolve) => {
|
|
1520
|
+
window.requestAnimationFrame(() => resolve());
|
|
1521
|
+
});
|
|
1522
|
+
if (token !== this.openToken) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const video = this.cameraVideo?.nativeElement;
|
|
1526
|
+
if (!video) {
|
|
1527
|
+
throw new Error('Не вдалося підготувати камеру');
|
|
1528
|
+
}
|
|
1529
|
+
if (await this.tryOpenNativeCamera(token, video)) {
|
|
1530
|
+
this.isCameraOpen.set(true);
|
|
1531
|
+
this.cameraStatus.set('Камера відкрита, наведіть на штрихкод');
|
|
1532
|
+
this.cameraStateChange.emit('open');
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
await this.openZxingCamera(token, video);
|
|
1536
|
+
if (token !== this.openToken) {
|
|
1537
|
+
this.cameraControls?.stop();
|
|
1538
|
+
this.cameraControls = null;
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
this.isCameraOpen.set(true);
|
|
1542
|
+
this.cameraStatus.set('Камера відкрита, наведіть на штрихкод');
|
|
1543
|
+
this.cameraStateChange.emit('open');
|
|
1544
|
+
}
|
|
1545
|
+
catch (error) {
|
|
1546
|
+
if (token !== this.openToken) {
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
this.cameraError.set(error?.message || 'Не вдалося відкрити камеру');
|
|
1550
|
+
this.cameraStatus.set('Камеру не відкрито');
|
|
1551
|
+
this.cameraStateChange.emit('error');
|
|
1552
|
+
this.stopCamera();
|
|
1553
|
+
}
|
|
1554
|
+
finally {
|
|
1555
|
+
if (token === this.openToken) {
|
|
1556
|
+
this.isCameraStarting.set(false);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
closeCamera() {
|
|
1561
|
+
this.stopCamera();
|
|
1562
|
+
}
|
|
1563
|
+
scheduleAutoOpen() {
|
|
1564
|
+
if (typeof window === 'undefined' || !this.autoOpen() || this.disabled() || this.isCameraOpen() || this.isCameraStarting() || this.autoOpenQueued) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
this.autoOpenQueued = true;
|
|
1568
|
+
window.requestAnimationFrame(() => {
|
|
1569
|
+
this.autoOpenQueued = false;
|
|
1570
|
+
if (!this.autoOpen() || this.disabled() || this.isCameraOpen() || this.isCameraStarting()) {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
void this.openCamera();
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
onBarcodeDetected(value) {
|
|
1577
|
+
this.valueChange.emit(value);
|
|
1578
|
+
this.scanned.emit(value);
|
|
1579
|
+
this.cameraStatus.set('Штрихкод знайдено');
|
|
1580
|
+
this.cameraControls?.stop();
|
|
1581
|
+
this.cameraControls = null;
|
|
1582
|
+
this.stopCamera();
|
|
1583
|
+
}
|
|
1584
|
+
stopCamera() {
|
|
1585
|
+
this.openToken += 1;
|
|
1586
|
+
if (this.scanLoopId != null) {
|
|
1587
|
+
window.cancelAnimationFrame(this.scanLoopId);
|
|
1588
|
+
this.scanLoopId = null;
|
|
1589
|
+
}
|
|
1590
|
+
this.cameraControls?.stop();
|
|
1591
|
+
this.cameraControls = null;
|
|
1592
|
+
if (this.cameraStream) {
|
|
1593
|
+
for (const track of this.cameraStream.getTracks()) {
|
|
1594
|
+
track.stop();
|
|
1595
|
+
}
|
|
1596
|
+
this.cameraStream = null;
|
|
1597
|
+
}
|
|
1598
|
+
const video = this.cameraVideo?.nativeElement;
|
|
1599
|
+
if (video) {
|
|
1600
|
+
video.pause();
|
|
1601
|
+
video.srcObject = null;
|
|
1602
|
+
}
|
|
1603
|
+
this.nativeBarcodeDetector = null;
|
|
1604
|
+
this.isCameraOpen.set(false);
|
|
1605
|
+
this.isCameraStarting.set(false);
|
|
1606
|
+
this.cameraStateChange.emit(this.cameraError() ? 'error' : 'idle');
|
|
1607
|
+
if (!this.cameraError()) {
|
|
1608
|
+
this.cameraStatus.set('Камеру зупинено');
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
async tryOpenNativeCamera(token, video) {
|
|
1612
|
+
const detector = await this.createNativeBarcodeDetector();
|
|
1613
|
+
if (!detector) {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
try {
|
|
1617
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1618
|
+
audio: false,
|
|
1619
|
+
video: {
|
|
1620
|
+
facingMode: { ideal: 'environment' },
|
|
1621
|
+
width: { ideal: 1280 },
|
|
1622
|
+
height: { ideal: 720 },
|
|
1623
|
+
},
|
|
1624
|
+
});
|
|
1625
|
+
if (token !== this.openToken) {
|
|
1626
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
1627
|
+
return true;
|
|
1628
|
+
}
|
|
1629
|
+
this.nativeBarcodeDetector = detector;
|
|
1630
|
+
this.cameraStream = stream;
|
|
1631
|
+
video.srcObject = stream;
|
|
1632
|
+
await video.play().catch(() => undefined);
|
|
1633
|
+
this.scheduleNativeScan(token);
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
catch {
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async openZxingCamera(token, video) {
|
|
1641
|
+
this.cameraControls = await this.barcodeReader.decodeFromConstraints({
|
|
1642
|
+
audio: false,
|
|
1643
|
+
video: {
|
|
1644
|
+
facingMode: { ideal: 'environment' },
|
|
1645
|
+
width: { ideal: 960 },
|
|
1646
|
+
height: { ideal: 540 },
|
|
1647
|
+
},
|
|
1648
|
+
}, video, (result, error) => {
|
|
1649
|
+
if (token !== this.openToken) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
if (error || !result) {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const value = String(result.getText()).trim();
|
|
1656
|
+
if (value) {
|
|
1657
|
+
this.onBarcodeDetected(value);
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
scheduleNativeScan(token) {
|
|
1662
|
+
if (typeof window === 'undefined') {
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const loop = async () => {
|
|
1666
|
+
if (token !== this.openToken) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const video = this.cameraVideo?.nativeElement;
|
|
1670
|
+
const detector = this.nativeBarcodeDetector;
|
|
1671
|
+
if (!video || !detector || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
1672
|
+
this.scanLoopId = window.requestAnimationFrame(() => {
|
|
1673
|
+
void loop();
|
|
1674
|
+
});
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
try {
|
|
1678
|
+
const barcodes = await detector.detect(video);
|
|
1679
|
+
const value = String(barcodes?.[0]?.rawValue || '').trim();
|
|
1680
|
+
if (value) {
|
|
1681
|
+
this.onBarcodeDetected(value);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
catch {
|
|
1686
|
+
// Native detection can fail on a frame; keep scanning.
|
|
1687
|
+
}
|
|
1688
|
+
this.scanLoopId = window.requestAnimationFrame(() => {
|
|
1689
|
+
void loop();
|
|
1690
|
+
});
|
|
1691
|
+
};
|
|
1692
|
+
this.scanLoopId = window.requestAnimationFrame(() => {
|
|
1693
|
+
void loop();
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
async createNativeBarcodeDetector() {
|
|
1697
|
+
if (typeof window === 'undefined') {
|
|
1698
|
+
return null;
|
|
1699
|
+
}
|
|
1700
|
+
const detectorCtor = window.BarcodeDetector;
|
|
1701
|
+
if (!detectorCtor) {
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
if (detectorCtor.getSupportedFormats) {
|
|
1706
|
+
const supportedFormats = await detectorCtor.getSupportedFormats();
|
|
1707
|
+
if (!supportedFormats.includes('code_128')) {
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return new detectorCtor({ formats: ['code_128'] });
|
|
1712
|
+
}
|
|
1713
|
+
catch {
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiBarcodeScannerInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1718
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiBarcodeScannerInputComponent, isStandalone: true, selector: "forms-barcode-scanner-input", 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: `
|
|
1719
|
+
<div class="space-y-2">
|
|
1720
|
+
@if (label()) {
|
|
1721
|
+
<label class="flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500" for="barcode-scanner-input">
|
|
1722
|
+
<span class="text-[0.7rem] font-semibold text-slate-600">
|
|
1723
|
+
{{ label() }}
|
|
1724
|
+
</span>
|
|
1725
|
+
</label>
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
<div class="flex items-start gap-2">
|
|
1729
|
+
<div class="min-w-0 flex-1">
|
|
1730
|
+
<input
|
|
1731
|
+
#barcodeInput
|
|
1732
|
+
id="barcode-scanner-input"
|
|
1733
|
+
type="text"
|
|
1734
|
+
autocomplete="off"
|
|
1735
|
+
spellcheck="false"
|
|
1736
|
+
[disabled]="disabled()"
|
|
1737
|
+
[ngModel]="value()"
|
|
1738
|
+
(ngModelChange)="onValueChange($event)"
|
|
1739
|
+
(keydown.enter)="requestSearch()"
|
|
1740
|
+
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none"
|
|
1741
|
+
[placeholder]="placeholder()"
|
|
1742
|
+
/>
|
|
1743
|
+
@if (hint()) {
|
|
1744
|
+
<p class="mt-2 text-[0.5rem] text-slate-500">
|
|
1745
|
+
{{ hint() }}
|
|
1746
|
+
</p>
|
|
1747
|
+
}
|
|
1748
|
+
</div>
|
|
1749
|
+
|
|
1750
|
+
<div class="mt-0.5 flex flex-col gap-2">
|
|
1751
|
+
<ui-button
|
|
1752
|
+
type="button"
|
|
1753
|
+
variant="secondary"
|
|
1754
|
+
size="sm"
|
|
1755
|
+
[disabled]="disabled()"
|
|
1756
|
+
(click)="requestSearch()"
|
|
1757
|
+
>
|
|
1758
|
+
{{ searchButtonLabel() }}
|
|
1759
|
+
</ui-button>
|
|
1760
|
+
<ui-button
|
|
1761
|
+
type="button"
|
|
1762
|
+
variant="ghost"
|
|
1763
|
+
size="sm"
|
|
1764
|
+
[disabled]="disabled() || isCameraStarting()"
|
|
1765
|
+
(click)="isCameraOpen() ? closeCamera() : openCamera()"
|
|
1766
|
+
>
|
|
1767
|
+
{{ isCameraOpen() ? closeCameraLabel() : openCameraLabel() }}
|
|
1768
|
+
</ui-button>
|
|
1769
|
+
</div>
|
|
1770
|
+
</div>
|
|
1771
|
+
|
|
1772
|
+
@if (isCameraStarting()) {
|
|
1773
|
+
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-500">
|
|
1774
|
+
{{ cameraStartingLabel() }}
|
|
1775
|
+
</div>
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
@if (isCameraOpen()) {
|
|
1779
|
+
<section class="space-y-3 rounded-3xl border border-slate-200 bg-slate-950 p-3 text-white">
|
|
1780
|
+
<div class="flex items-center justify-between gap-3">
|
|
1781
|
+
<div>
|
|
1782
|
+
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-slate-300">{{ cameraTitle() }}</p>
|
|
1783
|
+
<p class="text-sm text-slate-200">{{ cameraStatus() }}</p>
|
|
1784
|
+
</div>
|
|
1785
|
+
<button
|
|
1786
|
+
type="button"
|
|
1787
|
+
class="rounded-full border border-slate-700 px-3 py-1 text-xs font-semibold text-slate-200 transition hover:border-slate-500 hover:text-white"
|
|
1788
|
+
(click)="closeCamera()"
|
|
1789
|
+
>
|
|
1790
|
+
{{ closeCameraLabel() }}
|
|
1791
|
+
</button>
|
|
1792
|
+
</div>
|
|
1793
|
+
|
|
1794
|
+
<div class="overflow-hidden rounded-2xl bg-black">
|
|
1795
|
+
<video #cameraVideo class="h-[260px] w-full object-cover" autoplay muted playsinline></video>
|
|
1796
|
+
</div>
|
|
1797
|
+
|
|
1798
|
+
<p class="text-xs text-slate-300">
|
|
1799
|
+
{{ cameraHint() }}
|
|
1800
|
+
</p>
|
|
1801
|
+
</section>
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
@if (cameraError()) {
|
|
1805
|
+
<div class="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
1806
|
+
{{ cameraError() }}
|
|
1807
|
+
</div>
|
|
1808
|
+
}
|
|
1809
|
+
</div>
|
|
1810
|
+
`, 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"] }] });
|
|
1811
|
+
}
|
|
1812
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiBarcodeScannerInputComponent, decorators: [{
|
|
1813
|
+
type: Component,
|
|
1814
|
+
args: [{
|
|
1815
|
+
selector: 'forms-barcode-scanner-input',
|
|
1816
|
+
standalone: true,
|
|
1817
|
+
imports: [CommonModule, FormsModule, UiButtonComponent],
|
|
1818
|
+
template: `
|
|
1819
|
+
<div class="space-y-2">
|
|
1820
|
+
@if (label()) {
|
|
1821
|
+
<label class="flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500" for="barcode-scanner-input">
|
|
1822
|
+
<span class="text-[0.7rem] font-semibold text-slate-600">
|
|
1823
|
+
{{ label() }}
|
|
1824
|
+
</span>
|
|
1825
|
+
</label>
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
<div class="flex items-start gap-2">
|
|
1829
|
+
<div class="min-w-0 flex-1">
|
|
1830
|
+
<input
|
|
1831
|
+
#barcodeInput
|
|
1832
|
+
id="barcode-scanner-input"
|
|
1833
|
+
type="text"
|
|
1834
|
+
autocomplete="off"
|
|
1835
|
+
spellcheck="false"
|
|
1836
|
+
[disabled]="disabled()"
|
|
1837
|
+
[ngModel]="value()"
|
|
1838
|
+
(ngModelChange)="onValueChange($event)"
|
|
1839
|
+
(keydown.enter)="requestSearch()"
|
|
1840
|
+
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none"
|
|
1841
|
+
[placeholder]="placeholder()"
|
|
1842
|
+
/>
|
|
1843
|
+
@if (hint()) {
|
|
1844
|
+
<p class="mt-2 text-[0.5rem] text-slate-500">
|
|
1845
|
+
{{ hint() }}
|
|
1846
|
+
</p>
|
|
1847
|
+
}
|
|
1848
|
+
</div>
|
|
1849
|
+
|
|
1850
|
+
<div class="mt-0.5 flex flex-col gap-2">
|
|
1851
|
+
<ui-button
|
|
1852
|
+
type="button"
|
|
1853
|
+
variant="secondary"
|
|
1854
|
+
size="sm"
|
|
1855
|
+
[disabled]="disabled()"
|
|
1856
|
+
(click)="requestSearch()"
|
|
1857
|
+
>
|
|
1858
|
+
{{ searchButtonLabel() }}
|
|
1859
|
+
</ui-button>
|
|
1860
|
+
<ui-button
|
|
1861
|
+
type="button"
|
|
1862
|
+
variant="ghost"
|
|
1863
|
+
size="sm"
|
|
1864
|
+
[disabled]="disabled() || isCameraStarting()"
|
|
1865
|
+
(click)="isCameraOpen() ? closeCamera() : openCamera()"
|
|
1866
|
+
>
|
|
1867
|
+
{{ isCameraOpen() ? closeCameraLabel() : openCameraLabel() }}
|
|
1868
|
+
</ui-button>
|
|
1869
|
+
</div>
|
|
1870
|
+
</div>
|
|
1871
|
+
|
|
1872
|
+
@if (isCameraStarting()) {
|
|
1873
|
+
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-500">
|
|
1874
|
+
{{ cameraStartingLabel() }}
|
|
1875
|
+
</div>
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
@if (isCameraOpen()) {
|
|
1879
|
+
<section class="space-y-3 rounded-3xl border border-slate-200 bg-slate-950 p-3 text-white">
|
|
1880
|
+
<div class="flex items-center justify-between gap-3">
|
|
1881
|
+
<div>
|
|
1882
|
+
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-slate-300">{{ cameraTitle() }}</p>
|
|
1883
|
+
<p class="text-sm text-slate-200">{{ cameraStatus() }}</p>
|
|
1884
|
+
</div>
|
|
1885
|
+
<button
|
|
1886
|
+
type="button"
|
|
1887
|
+
class="rounded-full border border-slate-700 px-3 py-1 text-xs font-semibold text-slate-200 transition hover:border-slate-500 hover:text-white"
|
|
1888
|
+
(click)="closeCamera()"
|
|
1889
|
+
>
|
|
1890
|
+
{{ closeCameraLabel() }}
|
|
1891
|
+
</button>
|
|
1892
|
+
</div>
|
|
1893
|
+
|
|
1894
|
+
<div class="overflow-hidden rounded-2xl bg-black">
|
|
1895
|
+
<video #cameraVideo class="h-[260px] w-full object-cover" autoplay muted playsinline></video>
|
|
1896
|
+
</div>
|
|
1897
|
+
|
|
1898
|
+
<p class="text-xs text-slate-300">
|
|
1899
|
+
{{ cameraHint() }}
|
|
1900
|
+
</p>
|
|
1901
|
+
</section>
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
@if (cameraError()) {
|
|
1905
|
+
<div class="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
|
1906
|
+
{{ cameraError() }}
|
|
1907
|
+
</div>
|
|
1908
|
+
}
|
|
1909
|
+
</div>
|
|
1910
|
+
`,
|
|
1911
|
+
}]
|
|
1912
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autoOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoOpen", required: false }] }], searchButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchButtonLabel", required: false }] }], openCameraLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "openCameraLabel", required: false }] }], closeCameraLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeCameraLabel", required: false }] }], cameraTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameraTitle", required: false }] }], cameraStartingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameraStartingLabel", required: false }] }], cameraHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameraHint", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], scanned: [{ type: i0.Output, args: ["scanned"] }], searchRequested: [{ type: i0.Output, args: ["searchRequested"] }], cameraStateChange: [{ type: i0.Output, args: ["cameraStateChange"] }], cameraVideo: [{
|
|
1913
|
+
type: ViewChild,
|
|
1914
|
+
args: ['cameraVideo']
|
|
1915
|
+
}] } });
|
|
1916
|
+
|
|
1917
|
+
class UiCheckboxInputComponent {
|
|
1918
|
+
label;
|
|
1919
|
+
disabled = false;
|
|
1920
|
+
value = false;
|
|
1921
|
+
valueChange = new EventEmitter();
|
|
1922
|
+
onValueChange(event) {
|
|
1923
|
+
this.valueChange.emit(event.target.checked);
|
|
1924
|
+
}
|
|
1925
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1926
|
+
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 }] });
|
|
1927
|
+
}
|
|
1928
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, decorators: [{
|
|
1929
|
+
type: Component,
|
|
1930
|
+
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" }]
|
|
1931
|
+
}], propDecorators: { label: [{
|
|
1932
|
+
type: Input
|
|
1933
|
+
}], disabled: [{
|
|
1934
|
+
type: Input
|
|
1935
|
+
}], value: [{
|
|
1936
|
+
type: Input
|
|
1937
|
+
}], valueChange: [{
|
|
1938
|
+
type: Output
|
|
1939
|
+
}] } });
|
|
1940
|
+
|
|
1941
|
+
class UiColorInputComponent {
|
|
1942
|
+
cdr = inject(ChangeDetectorRef);
|
|
1943
|
+
label;
|
|
1944
|
+
hint;
|
|
1945
|
+
name;
|
|
1946
|
+
id;
|
|
1947
|
+
required = false;
|
|
1948
|
+
fullWidth = false;
|
|
1949
|
+
error;
|
|
1950
|
+
disabled = false;
|
|
1951
|
+
value = '#000000';
|
|
1952
|
+
static nextId = 0;
|
|
1953
|
+
generatedId = `ui-color-input-${UiColorInputComponent.nextId++}`;
|
|
1954
|
+
onChange = () => undefined;
|
|
1955
|
+
onTouched = () => undefined;
|
|
1956
|
+
get controlId() {
|
|
1957
|
+
return this.id ?? this.generatedId;
|
|
1958
|
+
}
|
|
1959
|
+
get pickerValue() {
|
|
1960
|
+
return this.isColorValue(this.value) ? this.value : '#000000';
|
|
1961
|
+
}
|
|
1962
|
+
writeValue(value) {
|
|
1963
|
+
this.value = value?.trim() || '#000000';
|
|
1964
|
+
this.cdr.markForCheck();
|
|
1965
|
+
}
|
|
1966
|
+
registerOnChange(fn) {
|
|
1967
|
+
this.onChange = fn;
|
|
1968
|
+
}
|
|
1969
|
+
registerOnTouched(fn) {
|
|
1970
|
+
this.onTouched = fn;
|
|
1971
|
+
}
|
|
1972
|
+
setDisabledState(isDisabled) {
|
|
1973
|
+
this.disabled = isDisabled;
|
|
1974
|
+
this.cdr.markForCheck();
|
|
1975
|
+
}
|
|
1976
|
+
onPickerChange(value) {
|
|
1977
|
+
this.value = value;
|
|
1978
|
+
this.onChange(value);
|
|
1979
|
+
}
|
|
1980
|
+
onHexInput(value) {
|
|
1981
|
+
this.value = this.normalizeHexInput(value);
|
|
1982
|
+
this.onChange(this.value);
|
|
1983
|
+
}
|
|
1984
|
+
markTouched() {
|
|
1985
|
+
this.onTouched();
|
|
1986
|
+
}
|
|
1987
|
+
normalizeHexInput(value) {
|
|
1988
|
+
const trimmed = value.trim();
|
|
1989
|
+
if (!trimmed) {
|
|
1990
|
+
return '';
|
|
1991
|
+
}
|
|
1992
|
+
return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
|
|
1993
|
+
}
|
|
1994
|
+
isColorValue(value) {
|
|
1995
|
+
return /^#([0-9a-fA-F]{6})$/.test(value.trim());
|
|
1996
|
+
}
|
|
1997
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiColorInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1998
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiColorInputComponent, isStandalone: true, selector: "forms-color-input", inputs: { label: "label", hint: "hint", name: "name", id: "id", required: "required", fullWidth: "fullWidth", error: "error" }, providers: [
|
|
1999
|
+
{
|
|
2000
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2001
|
+
useExisting: forwardRef(() => UiColorInputComponent),
|
|
2002
|
+
multi: true,
|
|
2003
|
+
},
|
|
2004
|
+
], 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-sky-400 focus:outline-none 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 });
|
|
2005
|
+
}
|
|
2006
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiColorInputComponent, decorators: [{
|
|
2007
|
+
type: Component,
|
|
2008
|
+
args: [{ selector: 'forms-color-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
2009
|
+
{
|
|
2010
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2011
|
+
useExisting: forwardRef(() => UiColorInputComponent),
|
|
2012
|
+
multi: true,
|
|
2013
|
+
},
|
|
2014
|
+
], 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-sky-400 focus:outline-none 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" }]
|
|
2015
|
+
}], propDecorators: { label: [{
|
|
2016
|
+
type: Input
|
|
2017
|
+
}], hint: [{
|
|
2018
|
+
type: Input
|
|
2019
|
+
}], name: [{
|
|
2020
|
+
type: Input
|
|
2021
|
+
}], id: [{
|
|
2022
|
+
type: Input
|
|
2023
|
+
}], required: [{
|
|
2024
|
+
type: Input
|
|
2025
|
+
}], fullWidth: [{
|
|
2026
|
+
type: Input
|
|
2027
|
+
}], error: [{
|
|
2028
|
+
type: Input
|
|
2029
|
+
}] } });
|
|
2030
|
+
|
|
2031
|
+
class UiFileInputComponent {
|
|
2032
|
+
label;
|
|
2033
|
+
hint;
|
|
2034
|
+
accept;
|
|
2035
|
+
multiple = false;
|
|
2036
|
+
disabled = false;
|
|
2037
|
+
value = null;
|
|
2038
|
+
valueChange = new EventEmitter();
|
|
2039
|
+
filesChange = new EventEmitter();
|
|
2040
|
+
get selectedFiles() {
|
|
2041
|
+
if (this.value instanceof File)
|
|
2042
|
+
return [this.value];
|
|
2043
|
+
return Array.isArray(this.value) ? this.value.filter((item) => item instanceof File) : [];
|
|
2044
|
+
}
|
|
2045
|
+
onFilesSelected(event) {
|
|
2046
|
+
const input = event.target;
|
|
2047
|
+
const files = Array.from(input.files ?? []);
|
|
2048
|
+
const value = this.multiple ? files : (files[0] ?? null);
|
|
2049
|
+
this.valueChange.emit(value);
|
|
2050
|
+
this.filesChange.emit(files);
|
|
2051
|
+
input.value = '';
|
|
2052
|
+
}
|
|
2053
|
+
remove(index) {
|
|
2054
|
+
const files = this.selectedFiles.filter((_file, fileIndex) => fileIndex !== index);
|
|
2055
|
+
const value = this.multiple ? files : (files[0] ?? null);
|
|
2056
|
+
this.valueChange.emit(value);
|
|
2057
|
+
this.filesChange.emit(files);
|
|
2058
|
+
}
|
|
2059
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2060
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiFileInputComponent, isStandalone: true, selector: "forms-file-input", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange" }, 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 />\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 }] });
|
|
2061
|
+
}
|
|
2062
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFileInputComponent, decorators: [{
|
|
2063
|
+
type: Component,
|
|
2064
|
+
args: [{ selector: 'forms-file-input', standalone: true, imports: [CommonModule], 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 />\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" }]
|
|
2065
|
+
}], propDecorators: { label: [{
|
|
2066
|
+
type: Input
|
|
2067
|
+
}], hint: [{
|
|
2068
|
+
type: Input
|
|
2069
|
+
}], accept: [{
|
|
2070
|
+
type: Input
|
|
2071
|
+
}], multiple: [{
|
|
2072
|
+
type: Input
|
|
2073
|
+
}], disabled: [{
|
|
2074
|
+
type: Input
|
|
2075
|
+
}], value: [{
|
|
2076
|
+
type: Input
|
|
2077
|
+
}], valueChange: [{
|
|
2078
|
+
type: Output
|
|
2079
|
+
}], filesChange: [{
|
|
2080
|
+
type: Output
|
|
2081
|
+
}] } });
|
|
2082
|
+
|
|
2083
|
+
class UiInputComponent {
|
|
2084
|
+
cdr = inject(ChangeDetectorRef);
|
|
2085
|
+
label;
|
|
2086
|
+
hint;
|
|
2087
|
+
placeholder;
|
|
2088
|
+
name;
|
|
2089
|
+
id;
|
|
2090
|
+
type = 'text';
|
|
2091
|
+
element = 'input';
|
|
2092
|
+
rows = 4;
|
|
2093
|
+
required = false;
|
|
2094
|
+
fullWidth = false;
|
|
2095
|
+
autocomplete;
|
|
2096
|
+
readonly = false;
|
|
2097
|
+
pattern;
|
|
2098
|
+
error;
|
|
2099
|
+
actions = [];
|
|
2100
|
+
disabled = false;
|
|
2101
|
+
value = '';
|
|
2102
|
+
static nextId = 0;
|
|
2103
|
+
generatedId = `ui-input-${UiInputComponent.nextId++}`;
|
|
2104
|
+
actionTriggered = new EventEmitter();
|
|
2105
|
+
get controlId() {
|
|
2106
|
+
return this.id ?? this.generatedId;
|
|
2107
|
+
}
|
|
2108
|
+
onChange = () => undefined;
|
|
2109
|
+
onTouched = () => undefined;
|
|
2110
|
+
writeValue(value) {
|
|
2111
|
+
this.value = value ?? '';
|
|
2112
|
+
this.cdr.markForCheck();
|
|
2113
|
+
}
|
|
2114
|
+
registerOnChange(fn) {
|
|
2115
|
+
this.onChange = fn;
|
|
2116
|
+
}
|
|
2117
|
+
registerOnTouched(fn) {
|
|
2118
|
+
this.onTouched = fn;
|
|
2119
|
+
}
|
|
2120
|
+
setDisabledState(isDisabled) {
|
|
2121
|
+
this.disabled = isDisabled;
|
|
2122
|
+
this.cdr.markForCheck();
|
|
2123
|
+
}
|
|
2124
|
+
onValueChange(value) {
|
|
2125
|
+
const parsed = this.type === 'number' ? (value === '' ? null : Number(value)) : value;
|
|
2126
|
+
this.value = parsed;
|
|
2127
|
+
this.onChange(parsed);
|
|
2128
|
+
}
|
|
2129
|
+
triggerAction(action) {
|
|
2130
|
+
this.actionTriggered.emit(action);
|
|
2131
|
+
}
|
|
2132
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2133
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiInputComponent, isStandalone: true, selector: "forms-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", type: "type", element: "element", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
|
|
2134
|
+
{
|
|
2135
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2136
|
+
useExisting: forwardRef(() => UiInputComponent),
|
|
2137
|
+
multi: true,
|
|
2138
|
+
},
|
|
2139
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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 });
|
|
2140
|
+
}
|
|
2141
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiInputComponent, decorators: [{
|
|
2142
|
+
type: Component,
|
|
2143
|
+
args: [{ selector: 'forms-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
2144
|
+
{
|
|
2145
|
+
provide: NG_VALUE_ACCESSOR,
|
|
2146
|
+
useExisting: forwardRef(() => UiInputComponent),
|
|
2147
|
+
multi: true,
|
|
2148
|
+
},
|
|
2149
|
+
], 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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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" }]
|
|
2150
|
+
}], propDecorators: { label: [{
|
|
2151
|
+
type: Input
|
|
2152
|
+
}], hint: [{
|
|
2153
|
+
type: Input
|
|
2154
|
+
}], placeholder: [{
|
|
2155
|
+
type: Input
|
|
2156
|
+
}], name: [{
|
|
2157
|
+
type: Input
|
|
2158
|
+
}], id: [{
|
|
2159
|
+
type: Input
|
|
2160
|
+
}], type: [{
|
|
2161
|
+
type: Input
|
|
2162
|
+
}], element: [{
|
|
2163
|
+
type: Input
|
|
2164
|
+
}], rows: [{
|
|
2165
|
+
type: Input
|
|
2166
|
+
}], required: [{
|
|
2167
|
+
type: Input
|
|
2168
|
+
}], fullWidth: [{
|
|
2169
|
+
type: Input
|
|
2170
|
+
}], autocomplete: [{
|
|
2171
|
+
type: Input
|
|
2172
|
+
}], readonly: [{
|
|
2173
|
+
type: Input
|
|
2174
|
+
}], pattern: [{
|
|
2175
|
+
type: Input
|
|
2176
|
+
}], error: [{
|
|
2177
|
+
type: Input
|
|
2178
|
+
}], actions: [{
|
|
2179
|
+
type: Input
|
|
2180
|
+
}], actionTriggered: [{
|
|
2181
|
+
type: Output
|
|
2182
|
+
}] } });
|
|
2183
|
+
|
|
2184
|
+
class UiPhotoInputComponent {
|
|
2185
|
+
label;
|
|
2186
|
+
hint;
|
|
2187
|
+
accept = 'image/*';
|
|
2188
|
+
multiple = false;
|
|
2189
|
+
limit = Number.MAX_SAFE_INTEGER;
|
|
2190
|
+
disabled = false;
|
|
2191
|
+
value = null;
|
|
2192
|
+
uploadPhoto = null;
|
|
2193
|
+
valueChange = new EventEmitter();
|
|
2194
|
+
filesChange = new EventEmitter();
|
|
2195
|
+
uploadingChange = new EventEmitter();
|
|
2196
|
+
pendingFiles = [];
|
|
2197
|
+
previews = [];
|
|
2198
|
+
uploading = false;
|
|
2199
|
+
fileTypes = new Map();
|
|
2200
|
+
ngOnChanges(changes) {
|
|
2201
|
+
if (changes['value'] || changes['multiple'])
|
|
2202
|
+
this.rebuildPreviews();
|
|
2203
|
+
}
|
|
2204
|
+
ngOnDestroy() {
|
|
2205
|
+
this.revokeBlobUrls();
|
|
2206
|
+
}
|
|
2207
|
+
get canSelectMore() {
|
|
2208
|
+
return !this.multiple || this.ids.length + this.pendingFiles.length < this.normalizedLimit;
|
|
2209
|
+
}
|
|
2210
|
+
onFilesSelected(event) {
|
|
2211
|
+
const input = event.target;
|
|
2212
|
+
const files = Array.from(input.files ?? []);
|
|
2213
|
+
input.value = '';
|
|
2214
|
+
if (!files.length || this.disabled)
|
|
2215
|
+
return;
|
|
2216
|
+
const accepted = this.multiple ? files.slice(0, Math.max(0, this.normalizedLimit - this.ids.length - this.pendingFiles.length)) : [files[0]];
|
|
2217
|
+
this.pendingFiles = this.multiple ? [...this.pendingFiles, ...accepted] : [accepted[0]];
|
|
2218
|
+
this.filesChange.emit(this.pendingFiles);
|
|
2219
|
+
this.rebuildPreviews();
|
|
2220
|
+
if (this.uploadPhoto)
|
|
2221
|
+
this.uploadFiles(accepted);
|
|
2222
|
+
}
|
|
2223
|
+
remove(index) {
|
|
2224
|
+
const ids = [...this.ids];
|
|
2225
|
+
if (index < ids.length) {
|
|
2226
|
+
ids.splice(index, 1);
|
|
2227
|
+
this.emitValue(this.multiple ? ids : (ids[0] ?? null));
|
|
2228
|
+
}
|
|
2229
|
+
else {
|
|
2230
|
+
this.pendingFiles.splice(index - ids.length, 1);
|
|
2231
|
+
this.filesChange.emit(this.pendingFiles);
|
|
2232
|
+
this.rebuildPreviews();
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
drop(event) {
|
|
2236
|
+
if (this.disabled || this.pendingFiles.length || this.ids.length < 2)
|
|
2237
|
+
return;
|
|
2238
|
+
const ids = [...this.ids];
|
|
2239
|
+
moveItemInArray(ids, event.previousIndex, event.currentIndex);
|
|
2240
|
+
this.emitValue(this.multiple ? ids : (ids[0] ?? null));
|
|
2241
|
+
}
|
|
2242
|
+
get ids() {
|
|
2243
|
+
if (typeof this.value === 'string')
|
|
2244
|
+
return this.value.trim() ? [this.value] : [];
|
|
2245
|
+
return Array.isArray(this.value) ? this.value.filter((item) => !!String(item || '').trim()) : [];
|
|
2246
|
+
}
|
|
2247
|
+
get normalizedLimit() {
|
|
2248
|
+
return Number.isInteger(Number(this.limit)) && Number(this.limit) > 0 ? Number(this.limit) : Number.MAX_SAFE_INTEGER;
|
|
2249
|
+
}
|
|
2250
|
+
emitValue(value) {
|
|
2251
|
+
this.valueChange.emit(value);
|
|
2252
|
+
this.rebuildPreviews(value);
|
|
2253
|
+
}
|
|
2254
|
+
uploadFiles(files) {
|
|
2255
|
+
if (!this.uploadPhoto || !files.length)
|
|
2256
|
+
return;
|
|
2257
|
+
this.uploading = true;
|
|
2258
|
+
this.uploadingChange.emit(true);
|
|
2259
|
+
forkJoin(files.map((file) => this.uploadPhoto(file))).subscribe({
|
|
2260
|
+
next: (results) => {
|
|
2261
|
+
const uploaded = results.map((item) => String(item.imageId || '').trim()).filter(Boolean);
|
|
2262
|
+
results.forEach((item) => {
|
|
2263
|
+
const id = String(item.imageId || '').trim();
|
|
2264
|
+
if (id && item.type)
|
|
2265
|
+
this.fileTypes.set(id, item.type);
|
|
2266
|
+
});
|
|
2267
|
+
const next = this.multiple ? [...this.ids, ...uploaded].slice(0, this.normalizedLimit) : (uploaded[0] ?? null);
|
|
2268
|
+
this.pendingFiles = this.pendingFiles.filter((file) => !files.includes(file));
|
|
2269
|
+
this.emitValue(next);
|
|
2270
|
+
this.filesChange.emit(this.pendingFiles);
|
|
2271
|
+
this.uploading = false;
|
|
2272
|
+
this.uploadingChange.emit(false);
|
|
2273
|
+
},
|
|
2274
|
+
error: () => {
|
|
2275
|
+
this.pendingFiles = this.pendingFiles.filter((file) => !files.includes(file));
|
|
2276
|
+
this.rebuildPreviews();
|
|
2277
|
+
this.uploading = false;
|
|
2278
|
+
this.uploadingChange.emit(false);
|
|
2279
|
+
},
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
rebuildPreviews(value = this.value) {
|
|
2283
|
+
this.revokeBlobUrls();
|
|
2284
|
+
const ids = typeof value === 'string' ? (value.trim() ? [value] : []) : (Array.isArray(value) ? value : []);
|
|
2285
|
+
this.previews = [
|
|
2286
|
+
...ids.map((id) => ({ name: id, url: this.resolveImageUrl(id), isImage: this.isPreviewableType(this.fileTypes.get(id)), source: 'id' })),
|
|
2287
|
+
...this.pendingFiles.map((file) => ({ name: file.name, url: URL.createObjectURL(file), isImage: file.type.startsWith('image/'), source: 'file' })),
|
|
2288
|
+
];
|
|
2289
|
+
}
|
|
2290
|
+
revokeBlobUrls() {
|
|
2291
|
+
for (const preview of this.previews)
|
|
2292
|
+
if (preview.url.startsWith('blob:'))
|
|
2293
|
+
URL.revokeObjectURL(preview.url);
|
|
2294
|
+
}
|
|
2295
|
+
resolveImageUrl(value) {
|
|
2296
|
+
if (/^(https?:\/\/|\/|data:)/i.test(value))
|
|
2297
|
+
return value;
|
|
2298
|
+
return `/api/files/${encodeURIComponent(value)}/preview?scale=25`;
|
|
2299
|
+
}
|
|
2300
|
+
isPreviewableType(value) {
|
|
2301
|
+
if (!value)
|
|
2302
|
+
return true;
|
|
2303
|
+
const normalized = value.toLowerCase();
|
|
2304
|
+
return normalized.startsWith('image/') || normalized === 'application/pdf';
|
|
2305
|
+
}
|
|
2306
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2307
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPhotoInputComponent, isStandalone: true, selector: "forms-photo-input", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", limit: "limit", disabled: "disabled", value: "value", uploadPhoto: "uploadPhoto" }, outputs: { valueChange: "valueChange", filesChange: "filesChange", uploadingChange: "uploadingChange" }, 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"] }] });
|
|
2308
|
+
}
|
|
2309
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoInputComponent, decorators: [{
|
|
2310
|
+
type: Component,
|
|
2311
|
+
args: [{ selector: 'forms-photo-input', standalone: true, imports: [CommonModule, DragDropModule], 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" }]
|
|
2312
|
+
}], propDecorators: { label: [{
|
|
2313
|
+
type: Input
|
|
2314
|
+
}], hint: [{
|
|
2315
|
+
type: Input
|
|
2316
|
+
}], accept: [{
|
|
2317
|
+
type: Input
|
|
2318
|
+
}], multiple: [{
|
|
2319
|
+
type: Input
|
|
2320
|
+
}], limit: [{
|
|
2321
|
+
type: Input
|
|
2322
|
+
}], disabled: [{
|
|
2323
|
+
type: Input
|
|
2324
|
+
}], value: [{
|
|
2325
|
+
type: Input
|
|
2326
|
+
}], uploadPhoto: [{
|
|
2327
|
+
type: Input
|
|
2328
|
+
}], valueChange: [{
|
|
2329
|
+
type: Output
|
|
2330
|
+
}], filesChange: [{
|
|
2331
|
+
type: Output
|
|
2332
|
+
}], uploadingChange: [{
|
|
2333
|
+
type: Output
|
|
2334
|
+
}] } });
|
|
2335
|
+
|
|
2336
|
+
class UiPhotoGalleryFieldComponent {
|
|
2337
|
+
label = 'Фото';
|
|
2338
|
+
orderLabel = 'Порядок фото (перетягуй)';
|
|
2339
|
+
imageIds = [];
|
|
2340
|
+
disabled = false;
|
|
2341
|
+
afterUpload = null;
|
|
2342
|
+
uploadPhoto = null;
|
|
2343
|
+
resolveImageUrl = null;
|
|
2344
|
+
notify = null;
|
|
2345
|
+
imageIdsChange = new EventEmitter();
|
|
2346
|
+
entries = [];
|
|
2347
|
+
imageEntrySequence = 0;
|
|
2348
|
+
get photoCount() {
|
|
2349
|
+
return this.entries.filter((entry) => !!entry.imageId || !!entry.file).length;
|
|
2350
|
+
}
|
|
2351
|
+
ngOnInit() {
|
|
2352
|
+
if (!this.entries.length)
|
|
2353
|
+
this.entries = this.normalizeImageEntries([]);
|
|
2354
|
+
}
|
|
2355
|
+
ngOnChanges(changes) {
|
|
2356
|
+
if (!changes['imageIds'])
|
|
2357
|
+
return;
|
|
2358
|
+
const nextIds = this.imageIds ?? [];
|
|
2359
|
+
const currentIds = this.entries.filter((entry) => !!entry.imageId).map((entry) => entry.imageId);
|
|
2360
|
+
const hasPendingLocalFiles = this.entries.some((entry) => !!entry.file || !!entry.isUploading);
|
|
2361
|
+
const sameIds = nextIds.length === currentIds.length && nextIds.every((id, idx) => id === currentIds[idx]);
|
|
2362
|
+
if (sameIds || hasPendingLocalFiles)
|
|
2363
|
+
return;
|
|
2364
|
+
this.entries = this.normalizeImageEntries(nextIds.map((imageId) => ({
|
|
2365
|
+
clientKey: imageId,
|
|
2366
|
+
imageId,
|
|
2367
|
+
preview: this.resolveImageUrl?.(imageId) || '',
|
|
2368
|
+
})));
|
|
2369
|
+
}
|
|
2370
|
+
ngOnDestroy() {
|
|
2371
|
+
this.entries.forEach((entry) => {
|
|
2372
|
+
if (entry.file && entry.preview)
|
|
2373
|
+
URL.revokeObjectURL(entry.preview);
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
onFilesChange(event, index) {
|
|
2377
|
+
const input = event.target;
|
|
2378
|
+
const files = Array.from(input.files ?? []);
|
|
2379
|
+
if (this.disabled || !files.length)
|
|
2380
|
+
return;
|
|
2381
|
+
const next = [...this.entries];
|
|
2382
|
+
const baseIndex = Math.max(0, Math.min(index, next.length - 1));
|
|
2383
|
+
files.forEach((file, offset) => {
|
|
2384
|
+
const targetIndex = baseIndex + offset;
|
|
2385
|
+
const existing = next[targetIndex];
|
|
2386
|
+
if (existing?.file && existing.preview)
|
|
2387
|
+
URL.revokeObjectURL(existing.preview);
|
|
2388
|
+
next[targetIndex] = {
|
|
2389
|
+
clientKey: existing?.clientKey ?? `local-${this.imageEntrySequence++}`,
|
|
2390
|
+
file,
|
|
2391
|
+
imageId: existing?.imageId,
|
|
2392
|
+
preview: URL.createObjectURL(file),
|
|
2393
|
+
};
|
|
2394
|
+
});
|
|
2395
|
+
this.entries = this.normalizeImageEntries(next);
|
|
2396
|
+
this.uploadPendingImages();
|
|
2397
|
+
input.value = '';
|
|
2398
|
+
}
|
|
2399
|
+
openFilePicker(input, event) {
|
|
2400
|
+
event?.preventDefault();
|
|
2401
|
+
event?.stopPropagation();
|
|
2402
|
+
if (!this.disabled && input)
|
|
2403
|
+
input.click();
|
|
2404
|
+
}
|
|
2405
|
+
onDrop(event) {
|
|
2406
|
+
if (this.disabled)
|
|
2407
|
+
return;
|
|
2408
|
+
const next = [...this.entries];
|
|
2409
|
+
const nonEmptyCount = next.filter((entry) => entry.imageId || entry.file).length;
|
|
2410
|
+
if (event.previousIndex >= nonEmptyCount || event.currentIndex >= nonEmptyCount) {
|
|
2411
|
+
this.entries = this.normalizeImageEntries(next);
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
moveItemInArray(next, event.previousIndex, event.currentIndex);
|
|
2415
|
+
this.entries = this.normalizeImageEntries(next);
|
|
2416
|
+
this.emitImageIds();
|
|
2417
|
+
}
|
|
2418
|
+
removeAt(index) {
|
|
2419
|
+
if (this.disabled || index < 0 || index >= this.entries.length)
|
|
2420
|
+
return;
|
|
2421
|
+
const next = [...this.entries];
|
|
2422
|
+
const [removed] = next.splice(index, 1);
|
|
2423
|
+
if (removed?.file && removed.preview)
|
|
2424
|
+
URL.revokeObjectURL(removed.preview);
|
|
2425
|
+
this.entries = this.normalizeImageEntries(next);
|
|
2426
|
+
this.emitImageIds();
|
|
2427
|
+
}
|
|
2428
|
+
createImageEntry() {
|
|
2429
|
+
return { clientKey: `local-${this.imageEntrySequence++}` };
|
|
2430
|
+
}
|
|
2431
|
+
normalizeImageEntries(entries) {
|
|
2432
|
+
const nonEmpty = entries.filter((entry) => !!entry?.imageId || !!entry?.file);
|
|
2433
|
+
return nonEmpty.length ? [...nonEmpty, this.createImageEntry()] : [this.createImageEntry()];
|
|
2434
|
+
}
|
|
2435
|
+
emitImageIds() {
|
|
2436
|
+
this.imageIdsChange.emit(this.entries.filter((entry) => !!entry.imageId).map((entry) => entry.imageId));
|
|
2437
|
+
}
|
|
2438
|
+
uploadPendingImages() {
|
|
2439
|
+
const nextIndex = this.entries.findIndex((entry) => !!entry.file && !entry.isUploading);
|
|
2440
|
+
if (nextIndex < 0 || !this.uploadPhoto)
|
|
2441
|
+
return;
|
|
2442
|
+
const entry = this.entries[nextIndex];
|
|
2443
|
+
entry.isUploading = true;
|
|
2444
|
+
const file = entry.file;
|
|
2445
|
+
this.uploadPhoto(file).subscribe({
|
|
2446
|
+
next: (response) => {
|
|
2447
|
+
const persist$ = this.afterUpload ? this.afterUpload(response.imageId) : of(null);
|
|
2448
|
+
persist$.subscribe({
|
|
2449
|
+
next: () => {
|
|
2450
|
+
entry.imageId = response.imageId;
|
|
2451
|
+
entry.file = undefined;
|
|
2452
|
+
entry.isUploading = undefined;
|
|
2453
|
+
entry.preview = this.resolveImageUrl?.(response.imageId) || entry.preview;
|
|
2454
|
+
this.entries = this.normalizeImageEntries([...this.entries]);
|
|
2455
|
+
this.emitImageIds();
|
|
2456
|
+
this.notify?.('Фото завантажено', 'success', 2000);
|
|
2457
|
+
this.uploadPendingImages();
|
|
2458
|
+
},
|
|
2459
|
+
error: (error) => {
|
|
2460
|
+
entry.isUploading = undefined;
|
|
2461
|
+
this.notify?.(error?.error?.message || 'Не вдалося привʼязати фото', 'error', 3500);
|
|
2462
|
+
this.uploadPendingImages();
|
|
2463
|
+
},
|
|
2464
|
+
});
|
|
2465
|
+
},
|
|
2466
|
+
error: (error) => {
|
|
2467
|
+
entry.isUploading = undefined;
|
|
2468
|
+
this.notify?.(error?.error?.message || 'Не вдалося завантажити фото', 'error', 3500);
|
|
2469
|
+
this.uploadPendingImages();
|
|
2470
|
+
},
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoGalleryFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2474
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPhotoGalleryFieldComponent, isStandalone: true, selector: "forms-photo-gallery-field", inputs: { label: "label", orderLabel: "orderLabel", imageIds: "imageIds", disabled: "disabled", afterUpload: "afterUpload", uploadPhoto: "uploadPhoto", 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"] }] });
|
|
2475
|
+
}
|
|
2476
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoGalleryFieldComponent, decorators: [{
|
|
2477
|
+
type: Component,
|
|
2478
|
+
args: [{ selector: 'forms-photo-gallery-field', 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"] }]
|
|
2479
|
+
}], propDecorators: { label: [{
|
|
2480
|
+
type: Input
|
|
2481
|
+
}], orderLabel: [{
|
|
2482
|
+
type: Input
|
|
2483
|
+
}], imageIds: [{
|
|
2484
|
+
type: Input
|
|
2485
|
+
}], disabled: [{
|
|
2486
|
+
type: Input
|
|
2487
|
+
}], afterUpload: [{
|
|
2488
|
+
type: Input
|
|
2489
|
+
}], uploadPhoto: [{
|
|
2490
|
+
type: Input
|
|
2491
|
+
}], resolveImageUrl: [{
|
|
2492
|
+
type: Input
|
|
2493
|
+
}], notify: [{
|
|
2494
|
+
type: Input
|
|
2495
|
+
}], imageIdsChange: [{
|
|
2496
|
+
type: Output
|
|
2497
|
+
}] } });
|
|
2498
|
+
|
|
2499
|
+
class UiRangeSliderComponent {
|
|
2500
|
+
min = 0;
|
|
2501
|
+
max = 100;
|
|
2502
|
+
step = 1;
|
|
2503
|
+
startValue = null;
|
|
2504
|
+
endValue = null;
|
|
2505
|
+
startLabel = 'Мінімальне значення';
|
|
2506
|
+
endLabel = 'Максимальне значення';
|
|
2507
|
+
startValueChange = new EventEmitter();
|
|
2508
|
+
endValueChange = new EventEmitter();
|
|
2509
|
+
interactionEnd = new EventEmitter();
|
|
2510
|
+
startInput;
|
|
2511
|
+
endInput;
|
|
2512
|
+
get safeStart() {
|
|
2513
|
+
const min = this.safeMin;
|
|
2514
|
+
const max = this.safeMax;
|
|
2515
|
+
const step = this.safeStep;
|
|
2516
|
+
const start = this.toFiniteNumber(this.startValue, min);
|
|
2517
|
+
const end = this.toFiniteNumber(this.endValue, max);
|
|
2518
|
+
return Math.max(min, Math.min(start, end - step));
|
|
2519
|
+
}
|
|
2520
|
+
get safeEnd() {
|
|
2521
|
+
const min = this.safeMin;
|
|
2522
|
+
const max = this.safeMax;
|
|
2523
|
+
const step = this.safeStep;
|
|
2524
|
+
const start = this.toFiniteNumber(this.startValue, min);
|
|
2525
|
+
const end = this.toFiniteNumber(this.endValue, max);
|
|
2526
|
+
return Math.min(max, Math.max(end, start + step));
|
|
2527
|
+
}
|
|
2528
|
+
get startPercent() {
|
|
2529
|
+
return this.toPercent(this.safeStart);
|
|
2530
|
+
}
|
|
2531
|
+
get endPercent() {
|
|
2532
|
+
return this.toPercent(this.safeEnd);
|
|
2533
|
+
}
|
|
2534
|
+
get rangePercent() {
|
|
2535
|
+
return Math.max(0, this.endPercent - this.startPercent);
|
|
2536
|
+
}
|
|
2537
|
+
get safeMin() {
|
|
2538
|
+
return this.toFiniteNumber(this.min, 0);
|
|
2539
|
+
}
|
|
2540
|
+
get safeMax() {
|
|
2541
|
+
const min = this.safeMin;
|
|
2542
|
+
const max = this.toFiniteNumber(this.max, min + this.safeStep);
|
|
2543
|
+
return max > min ? max : min + this.safeStep;
|
|
2544
|
+
}
|
|
2545
|
+
get safeStep() {
|
|
2546
|
+
const step = this.toFiniteNumber(this.step, 1);
|
|
2547
|
+
return step > 0 ? step : 1;
|
|
2548
|
+
}
|
|
2549
|
+
ngOnChanges(_) {
|
|
2550
|
+
this.syncNativeInputs();
|
|
2551
|
+
}
|
|
2552
|
+
ngAfterViewInit() {
|
|
2553
|
+
this.syncNativeInputs();
|
|
2554
|
+
}
|
|
2555
|
+
onStartInput(event) {
|
|
2556
|
+
const value = Number(event.target.value);
|
|
2557
|
+
this.startValueChange.emit(Math.min(value, this.safeEnd - this.safeStep));
|
|
2558
|
+
}
|
|
2559
|
+
onEndInput(event) {
|
|
2560
|
+
const value = Number(event.target.value);
|
|
2561
|
+
this.endValueChange.emit(Math.max(value, this.safeStart + this.safeStep));
|
|
2562
|
+
}
|
|
2563
|
+
toPercent(value) {
|
|
2564
|
+
const span = this.safeMax - this.safeMin;
|
|
2565
|
+
if (span <= 0) {
|
|
2566
|
+
return 0;
|
|
2567
|
+
}
|
|
2568
|
+
return ((value - this.safeMin) / span) * 100;
|
|
2569
|
+
}
|
|
2570
|
+
toFiniteNumber(value, fallback) {
|
|
2571
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
2572
|
+
}
|
|
2573
|
+
syncNativeInputs() {
|
|
2574
|
+
queueMicrotask(() => {
|
|
2575
|
+
this.syncNativeInput(this.startInput?.nativeElement, this.safeStart);
|
|
2576
|
+
this.syncNativeInput(this.endInput?.nativeElement, this.safeEnd);
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
syncNativeInput(input, value) {
|
|
2580
|
+
if (!input) {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
input.min = String(this.safeMin);
|
|
2584
|
+
input.max = String(this.safeMax);
|
|
2585
|
+
input.step = String(this.safeStep);
|
|
2586
|
+
input.value = String(value);
|
|
2587
|
+
}
|
|
2588
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiRangeSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2589
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiRangeSliderComponent, isStandalone: true, selector: "forms-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: `
|
|
2590
|
+
<div class="range-slider">
|
|
2591
|
+
<div class="range-slider__track"></div>
|
|
2592
|
+
<div
|
|
2593
|
+
class="range-slider__range"
|
|
2594
|
+
[style.left.%]="startPercent"
|
|
2595
|
+
[style.width.%]="rangePercent">
|
|
2596
|
+
</div>
|
|
2597
|
+
|
|
2598
|
+
<input
|
|
2599
|
+
#startInput
|
|
2600
|
+
class="range-slider__input"
|
|
2601
|
+
type="range"
|
|
2602
|
+
[min]="safeMin"
|
|
2603
|
+
[max]="safeMax"
|
|
2604
|
+
[step]="safeStep"
|
|
2605
|
+
[value]="safeStart"
|
|
2606
|
+
[attr.aria-label]="startLabel"
|
|
2607
|
+
(input)="onStartInput($event)"
|
|
2608
|
+
(change)="interactionEnd.emit()">
|
|
2609
|
+
|
|
2610
|
+
<input
|
|
2611
|
+
#endInput
|
|
2612
|
+
class="range-slider__input"
|
|
2613
|
+
type="range"
|
|
2614
|
+
[min]="safeMin"
|
|
2615
|
+
[max]="safeMax"
|
|
2616
|
+
[step]="safeStep"
|
|
2617
|
+
[value]="safeEnd"
|
|
2618
|
+
[attr.aria-label]="endLabel"
|
|
2619
|
+
(input)="onEndInput($event)"
|
|
2620
|
+
(change)="interactionEnd.emit()">
|
|
2621
|
+
</div>
|
|
2622
|
+
`, 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 }] });
|
|
2623
|
+
}
|
|
2624
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiRangeSliderComponent, decorators: [{
|
|
2625
|
+
type: Component,
|
|
2626
|
+
args: [{ selector: 'forms-range-slider', standalone: true, imports: [CommonModule], template: `
|
|
2627
|
+
<div class="range-slider">
|
|
2628
|
+
<div class="range-slider__track"></div>
|
|
2629
|
+
<div
|
|
2630
|
+
class="range-slider__range"
|
|
2631
|
+
[style.left.%]="startPercent"
|
|
2632
|
+
[style.width.%]="rangePercent">
|
|
2633
|
+
</div>
|
|
2634
|
+
|
|
2635
|
+
<input
|
|
2636
|
+
#startInput
|
|
2637
|
+
class="range-slider__input"
|
|
2638
|
+
type="range"
|
|
2639
|
+
[min]="safeMin"
|
|
2640
|
+
[max]="safeMax"
|
|
2641
|
+
[step]="safeStep"
|
|
2642
|
+
[value]="safeStart"
|
|
2643
|
+
[attr.aria-label]="startLabel"
|
|
2644
|
+
(input)="onStartInput($event)"
|
|
2645
|
+
(change)="interactionEnd.emit()">
|
|
2646
|
+
|
|
2647
|
+
<input
|
|
2648
|
+
#endInput
|
|
2649
|
+
class="range-slider__input"
|
|
2650
|
+
type="range"
|
|
2651
|
+
[min]="safeMin"
|
|
2652
|
+
[max]="safeMax"
|
|
2653
|
+
[step]="safeStep"
|
|
2654
|
+
[value]="safeEnd"
|
|
2655
|
+
[attr.aria-label]="endLabel"
|
|
2656
|
+
(input)="onEndInput($event)"
|
|
2657
|
+
(change)="interactionEnd.emit()">
|
|
2658
|
+
</div>
|
|
2659
|
+
`, 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"] }]
|
|
2660
|
+
}], propDecorators: { min: [{
|
|
2661
|
+
type: Input
|
|
2662
|
+
}], max: [{
|
|
2663
|
+
type: Input
|
|
2664
|
+
}], step: [{
|
|
2665
|
+
type: Input
|
|
2666
|
+
}], startValue: [{
|
|
2667
|
+
type: Input
|
|
2668
|
+
}], endValue: [{
|
|
2669
|
+
type: Input
|
|
2670
|
+
}], startLabel: [{
|
|
2671
|
+
type: Input
|
|
2672
|
+
}], endLabel: [{
|
|
2673
|
+
type: Input
|
|
2674
|
+
}], startValueChange: [{
|
|
2675
|
+
type: Output
|
|
2676
|
+
}], endValueChange: [{
|
|
2677
|
+
type: Output
|
|
2678
|
+
}], interactionEnd: [{
|
|
2679
|
+
type: Output
|
|
2680
|
+
}], startInput: [{
|
|
2681
|
+
type: ViewChild,
|
|
2682
|
+
args: ['startInput']
|
|
2683
|
+
}], endInput: [{
|
|
2684
|
+
type: ViewChild,
|
|
2685
|
+
args: ['endInput']
|
|
2686
|
+
}] } });
|
|
2687
|
+
|
|
2688
|
+
class UiSelectComponent {
|
|
2689
|
+
closeIcon = X;
|
|
2690
|
+
chevronDownIcon = ChevronDown;
|
|
2691
|
+
checkIcon = Check;
|
|
2692
|
+
static openInstance = null;
|
|
2693
|
+
cdr = inject(ChangeDetectorRef);
|
|
2694
|
+
destroyRef = inject(DestroyRef);
|
|
2695
|
+
http = inject(HttpClient);
|
|
2696
|
+
label;
|
|
2697
|
+
placeholder;
|
|
2698
|
+
name;
|
|
2699
|
+
data;
|
|
2700
|
+
required = false;
|
|
2701
|
+
fullWidth = false;
|
|
2702
|
+
disabled = false;
|
|
2703
|
+
multiple = false;
|
|
2704
|
+
includeBlankOption = false;
|
|
2705
|
+
blankOptionLabel = 'Оберіть…';
|
|
2706
|
+
emptyStateLabel = 'Немає варіантів для вибору';
|
|
2707
|
+
searchable = true;
|
|
2708
|
+
remoteSearch = false;
|
|
2709
|
+
searchPlaceholder = 'Пошук...';
|
|
2710
|
+
_options = [];
|
|
2711
|
+
set options(value) {
|
|
2712
|
+
this._options = value ?? [];
|
|
2713
|
+
this.updateSelectionFromValue();
|
|
2714
|
+
this.cdr.markForCheck();
|
|
2715
|
+
}
|
|
2716
|
+
get options() {
|
|
2717
|
+
return this._options;
|
|
2718
|
+
}
|
|
2719
|
+
error;
|
|
2720
|
+
hint;
|
|
2721
|
+
triggerClass = '';
|
|
2722
|
+
panelClass = '';
|
|
2723
|
+
valueChange = new EventEmitter();
|
|
2724
|
+
searchChange = new EventEmitter();
|
|
2725
|
+
dropdownTrigger;
|
|
2726
|
+
dropdownPositions = [
|
|
2727
|
+
{
|
|
2728
|
+
originX: 'start',
|
|
2729
|
+
originY: 'bottom',
|
|
2730
|
+
overlayX: 'start',
|
|
2731
|
+
overlayY: 'top',
|
|
2732
|
+
offsetY: 4,
|
|
2733
|
+
},
|
|
2734
|
+
{
|
|
2735
|
+
originX: 'start',
|
|
2736
|
+
originY: 'top',
|
|
2737
|
+
overlayX: 'start',
|
|
2738
|
+
overlayY: 'bottom',
|
|
2739
|
+
offsetY: -4,
|
|
2740
|
+
},
|
|
2741
|
+
];
|
|
2742
|
+
isOpen = false;
|
|
2743
|
+
triggerWidth = 0;
|
|
2744
|
+
value = '';
|
|
2745
|
+
multiValue = [];
|
|
2746
|
+
selectedOptions = [];
|
|
2747
|
+
searchTerm = '';
|
|
2748
|
+
isDictionaryLoading = false;
|
|
2749
|
+
searchChanges$ = new Subject();
|
|
2750
|
+
dictionaryRequestVersions = new Map();
|
|
2751
|
+
dictionaryOptions = [];
|
|
2752
|
+
onChange = () => undefined;
|
|
2753
|
+
onTouched = () => undefined;
|
|
2754
|
+
constructor() {
|
|
2755
|
+
this.searchChanges$.pipe(debounceTime(300), distinctUntilChanged()).subscribe((next) => {
|
|
2756
|
+
this.searchTerm = next;
|
|
2757
|
+
if (this.getDictionaryName()) {
|
|
2758
|
+
this.loadDictionaryOptions(next);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
this.updateSelectionFromValue();
|
|
2762
|
+
this.cdr.markForCheck();
|
|
2763
|
+
});
|
|
2764
|
+
this.destroyRef.onDestroy(() => {
|
|
2765
|
+
this.searchChanges$.complete();
|
|
2766
|
+
this.dictionaryRequestVersions.clear();
|
|
2767
|
+
if (UiSelectComponent.openInstance === this) {
|
|
2768
|
+
UiSelectComponent.openInstance = null;
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
ngOnChanges(changes) {
|
|
2773
|
+
if ('data' in changes) {
|
|
2774
|
+
this.reloadDictionaryOptions();
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
writeValue(value) {
|
|
2778
|
+
if (this.multiple) {
|
|
2779
|
+
this.multiValue = Array.isArray(value) ? value : [];
|
|
2780
|
+
}
|
|
2781
|
+
else {
|
|
2782
|
+
this.value = value ?? '';
|
|
2783
|
+
}
|
|
2784
|
+
this.updateSelectionFromValue();
|
|
2785
|
+
this.ensureSelectedDictionaryOptionsLoaded();
|
|
2786
|
+
this.cdr.markForCheck();
|
|
2787
|
+
}
|
|
2788
|
+
registerOnChange(fn) {
|
|
2789
|
+
this.onChange = fn;
|
|
2790
|
+
}
|
|
2791
|
+
registerOnTouched(fn) {
|
|
2792
|
+
this.onTouched = fn;
|
|
2793
|
+
}
|
|
2794
|
+
setDisabledState(isDisabled) {
|
|
2795
|
+
this.disabled = isDisabled;
|
|
2796
|
+
this.cdr.markForCheck();
|
|
2797
|
+
}
|
|
2798
|
+
toggleDropdown(event) {
|
|
2799
|
+
event.stopPropagation();
|
|
2800
|
+
if (this.disabled) {
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
if (this.isOpen) {
|
|
2804
|
+
this.closeDropdown();
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
if (UiSelectComponent.openInstance && UiSelectComponent.openInstance !== this) {
|
|
2808
|
+
UiSelectComponent.openInstance.closeDropdown();
|
|
2809
|
+
}
|
|
2810
|
+
this.triggerWidth = this.dropdownTrigger?.nativeElement.getBoundingClientRect().width ?? 0;
|
|
2811
|
+
this.isOpen = true;
|
|
2812
|
+
this.searchTerm = '';
|
|
2813
|
+
this.searchChanges$.next('');
|
|
2814
|
+
UiSelectComponent.openInstance = this;
|
|
2815
|
+
this.cdr.markForCheck();
|
|
2816
|
+
}
|
|
2817
|
+
selectOption(option, event) {
|
|
2818
|
+
event.stopPropagation();
|
|
2819
|
+
const key = this.getOptionValue(option);
|
|
2820
|
+
if (this.multiple) {
|
|
2821
|
+
const normalized = this.multiValue.includes(key)
|
|
2822
|
+
? this.multiValue.filter((item) => item !== key)
|
|
2823
|
+
: [...this.multiValue, key];
|
|
2824
|
+
this.multiValue = normalized;
|
|
2825
|
+
this.onChange(normalized);
|
|
2826
|
+
this.valueChange.emit(normalized);
|
|
2827
|
+
}
|
|
2828
|
+
else {
|
|
2829
|
+
const nextValue = this.value === key ? null : key;
|
|
2830
|
+
this.value = nextValue;
|
|
2831
|
+
this.onChange(nextValue);
|
|
2832
|
+
this.valueChange.emit(nextValue);
|
|
2833
|
+
this.closeDropdown();
|
|
2834
|
+
}
|
|
2835
|
+
this.updateSelectionFromValue();
|
|
2836
|
+
}
|
|
2837
|
+
markTouched() {
|
|
2838
|
+
this.onTouched();
|
|
2839
|
+
}
|
|
2840
|
+
closeDropdown() {
|
|
2841
|
+
if (!this.isOpen) {
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
this.isOpen = false;
|
|
2845
|
+
this.onTouched();
|
|
2846
|
+
this.searchTerm = '';
|
|
2847
|
+
this.searchChanges$.next('');
|
|
2848
|
+
if (UiSelectComponent.openInstance === this) {
|
|
2849
|
+
UiSelectComponent.openInstance = null;
|
|
2850
|
+
}
|
|
2851
|
+
this.cdr.markForCheck();
|
|
2852
|
+
}
|
|
2853
|
+
get displayLabel() {
|
|
2854
|
+
if (this.multiple) {
|
|
2855
|
+
if (!this.selectedOptions.length) {
|
|
2856
|
+
return this.placeholder ?? this.blankOptionLabel;
|
|
2857
|
+
}
|
|
2858
|
+
return `${this.selectedOptions.length}`;
|
|
2859
|
+
}
|
|
2860
|
+
return this.selectedOptions[0]?.label ?? this.placeholder ?? this.blankOptionLabel;
|
|
2861
|
+
}
|
|
2862
|
+
isSelected(option) {
|
|
2863
|
+
const value = this.getOptionValue(option);
|
|
2864
|
+
if (this.multiple) {
|
|
2865
|
+
return this.multiValue.includes(value);
|
|
2866
|
+
}
|
|
2867
|
+
return this.value === value;
|
|
2868
|
+
}
|
|
2869
|
+
getOptionValue(option) {
|
|
2870
|
+
return option.value ?? '';
|
|
2871
|
+
}
|
|
2872
|
+
trackOption(index, option) {
|
|
2873
|
+
return `${this.getOptionValue(option)}::${String(option.label || '').trim()}::${index}`;
|
|
2874
|
+
}
|
|
2875
|
+
get visibleOptions() {
|
|
2876
|
+
const options = this.getRenderOptions();
|
|
2877
|
+
if (!this.searchable || this.remoteSearch || !this.searchTerm.trim()) {
|
|
2878
|
+
return options;
|
|
2879
|
+
}
|
|
2880
|
+
const search = this.searchTerm.trim().toLowerCase();
|
|
2881
|
+
return options.filter((option) => {
|
|
2882
|
+
if (this.includeBlankOption && !this.multiple && this.isBlankOption(option)) {
|
|
2883
|
+
return true;
|
|
2884
|
+
}
|
|
2885
|
+
return String(option.label ?? '').toLowerCase().includes(search);
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
onSearchChange(value) {
|
|
2889
|
+
const next = String(value ?? '');
|
|
2890
|
+
this.searchChanges$.next(next);
|
|
2891
|
+
this.searchChange.emit(next);
|
|
2892
|
+
}
|
|
2893
|
+
removeOption(option, event) {
|
|
2894
|
+
event.stopPropagation();
|
|
2895
|
+
const key = this.getOptionValue(option);
|
|
2896
|
+
if (this.multiple) {
|
|
2897
|
+
this.multiValue = this.multiValue.filter((v) => v !== key);
|
|
2898
|
+
this.onChange(this.multiValue);
|
|
2899
|
+
this.valueChange.emit(this.multiValue);
|
|
2900
|
+
this.updateSelectionFromValue();
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
updateSelectionFromValue() {
|
|
2904
|
+
const options = this.getRenderOptions();
|
|
2905
|
+
if (this.multiple) {
|
|
2906
|
+
this.selectedOptions = options.filter((option) => this.multiValue.includes(this.getOptionValue(option)));
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
const current = options.find((option) => this.getOptionValue(option) === (this.value ?? ''));
|
|
2910
|
+
this.selectedOptions = current ? [current] : [];
|
|
2911
|
+
}
|
|
2912
|
+
getRenderOptions() {
|
|
2913
|
+
const merged = this.mergeOptions(this.options, this.dictionaryOptions);
|
|
2914
|
+
if (!this.includeBlankOption || this.multiple) {
|
|
2915
|
+
return merged;
|
|
2916
|
+
}
|
|
2917
|
+
return [{ label: this.blankOptionLabel, value: '' }, ...merged];
|
|
2918
|
+
}
|
|
2919
|
+
mergeOptions(...optionSets) {
|
|
2920
|
+
const merged = new Map();
|
|
2921
|
+
for (const options of optionSets) {
|
|
2922
|
+
for (const option of options || []) {
|
|
2923
|
+
const value = this.getOptionValue(option);
|
|
2924
|
+
const key = `${value}::${String(option.label || '').trim()}`;
|
|
2925
|
+
if (!merged.has(key)) {
|
|
2926
|
+
merged.set(key, option);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
return [...merged.values()];
|
|
2931
|
+
}
|
|
2932
|
+
isBlankOption(option) {
|
|
2933
|
+
return String(option.value ?? '') === '';
|
|
2934
|
+
}
|
|
2935
|
+
getDictionaryName() {
|
|
2936
|
+
return String(this.data || '').trim();
|
|
2937
|
+
}
|
|
2938
|
+
reloadDictionaryOptions() {
|
|
2939
|
+
const dictionaryName = this.getDictionaryName();
|
|
2940
|
+
this.dictionaryRequestVersions.clear();
|
|
2941
|
+
this.dictionaryOptions = [];
|
|
2942
|
+
this.isDictionaryLoading = false;
|
|
2943
|
+
if (!dictionaryName) {
|
|
2944
|
+
this.updateSelectionFromValue();
|
|
2945
|
+
this.cdr.markForCheck();
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
this.loadDictionaryOptions(this.searchTerm);
|
|
2949
|
+
}
|
|
2950
|
+
loadDictionaryOptions(query) {
|
|
2951
|
+
const dictionaryName = this.getDictionaryName();
|
|
2952
|
+
if (!dictionaryName) {
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
const requestVersion = (this.dictionaryRequestVersions.get(dictionaryName) || 0) + 1;
|
|
2956
|
+
this.dictionaryRequestVersions.set(dictionaryName, requestVersion);
|
|
2957
|
+
this.isDictionaryLoading = true;
|
|
2958
|
+
this.loadSuggestItems(dictionaryName, String(query || '').trim()).subscribe({
|
|
2959
|
+
next: (items) => {
|
|
2960
|
+
if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
this.dictionaryOptions = this.normalizeSuggestItems(items);
|
|
2964
|
+
this.isDictionaryLoading = false;
|
|
2965
|
+
this.ensureSelectedDictionaryOptionsLoaded();
|
|
2966
|
+
this.updateSelectionFromValue();
|
|
2967
|
+
this.cdr.markForCheck();
|
|
2968
|
+
},
|
|
2969
|
+
error: () => {
|
|
2970
|
+
if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
this.dictionaryOptions = [];
|
|
2974
|
+
this.isDictionaryLoading = false;
|
|
2975
|
+
this.updateSelectionFromValue();
|
|
2976
|
+
this.cdr.markForCheck();
|
|
2977
|
+
},
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
ensureSelectedDictionaryOptionsLoaded() {
|
|
2981
|
+
const dictionaryName = this.getDictionaryName();
|
|
2982
|
+
if (!dictionaryName) {
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const existingValues = new Set(this.getRenderOptions().map((option) => this.getOptionValue(option)));
|
|
2986
|
+
const selectedValues = this.multiple ? this.multiValue : [this.value];
|
|
2987
|
+
const missingValues = selectedValues
|
|
2988
|
+
.map((item) => String(item || '').trim())
|
|
2989
|
+
.filter((item) => !!item && !existingValues.has(item));
|
|
2990
|
+
if (!missingValues.length) {
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
forkJoin(missingValues.map((value) => this.loadSuggestItem(dictionaryName, value))).subscribe({
|
|
2994
|
+
next: (items) => {
|
|
2995
|
+
const nextOptions = [...this.dictionaryOptions];
|
|
2996
|
+
for (const item of items) {
|
|
2997
|
+
const normalized = this.normalizeSuggestItem(item);
|
|
2998
|
+
if (!nextOptions.some((option) => this.getOptionValue(option) === normalized.value)) {
|
|
2999
|
+
nextOptions.push(normalized);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
this.dictionaryOptions = nextOptions;
|
|
3003
|
+
this.updateSelectionFromValue();
|
|
3004
|
+
this.cdr.markForCheck();
|
|
3005
|
+
},
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
normalizeSuggestItems(items) {
|
|
3009
|
+
return (items || [])
|
|
3010
|
+
.map((item) => this.normalizeSuggestItem(item))
|
|
3011
|
+
.filter((item) => !!item.label || item.value !== null);
|
|
3012
|
+
}
|
|
3013
|
+
normalizeSuggestItem(item) {
|
|
3014
|
+
return {
|
|
3015
|
+
label: String(item?.label ?? ''),
|
|
3016
|
+
value: item?.key != null ? String(item.key) : null,
|
|
3017
|
+
imageUrl: item?.imageId ? `/api/files/${item.imageId}/preview?scale=25` : undefined,
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
loadSuggestItems(name, query) {
|
|
3021
|
+
const params = new URLSearchParams();
|
|
3022
|
+
if (query?.trim())
|
|
3023
|
+
params.set('q', query.trim());
|
|
3024
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
3025
|
+
return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
|
|
3026
|
+
}
|
|
3027
|
+
loadSuggestItem(name, key) {
|
|
3028
|
+
return this.http.get(`/api/suggest/${encodeURIComponent(name)}/${encodeURIComponent(key)}`);
|
|
3029
|
+
}
|
|
3030
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3031
|
+
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" }, providers: [
|
|
3032
|
+
{
|
|
3033
|
+
provide: NG_VALUE_ACCESSOR,
|
|
3034
|
+
useExisting: forwardRef(() => UiSelectComponent),
|
|
3035
|
+
multi: true,
|
|
3036
|
+
},
|
|
3037
|
+
], 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 w-full flex-wrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 transition hover:border-slate-300 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 <!-- 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 items-center gap-1 rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <button\n type=\"button\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"text-[10px]\"></lucide-icon>\n </button>\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=\"truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"ml-auto text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayWidth]=\"triggerWidth || 0\"\n [cdkConnectedOverlayViewportMargin]=\"8\"\n (backdropClick)=\"closeDropdown()\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 max-h-60 w-full overflow-auto rounded-xl border border-slate-200 bg-white shadow-lg\"\n [class]=\"panelClass\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-300 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 @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-center justify-between 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 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=\"truncate\">{{ 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\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$2.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 });
|
|
3038
|
+
}
|
|
3039
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSelectComponent, decorators: [{
|
|
3040
|
+
type: Component,
|
|
3041
|
+
args: [{ selector: 'forms-select', standalone: true, imports: [LucideAngularModule, CommonModule, FormsModule, CdkConnectedOverlay, CdkOverlayOrigin], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
3042
|
+
{
|
|
3043
|
+
provide: NG_VALUE_ACCESSOR,
|
|
3044
|
+
useExisting: forwardRef(() => UiSelectComponent),
|
|
3045
|
+
multi: true,
|
|
3046
|
+
},
|
|
3047
|
+
], 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 w-full flex-wrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 transition hover:border-slate-300 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 <!-- 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 items-center gap-1 rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <button\n type=\"button\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"text-[10px]\"></lucide-icon>\n </button>\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=\"truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"ml-auto text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayWidth]=\"triggerWidth || 0\"\n [cdkConnectedOverlayViewportMargin]=\"8\"\n (backdropClick)=\"closeDropdown()\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 max-h-60 w-full overflow-auto rounded-xl border border-slate-200 bg-white shadow-lg\"\n [class]=\"panelClass\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-300 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 @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-center justify-between 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 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=\"truncate\">{{ 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\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" }]
|
|
3048
|
+
}], ctorParameters: () => [], propDecorators: { label: [{
|
|
3049
|
+
type: Input
|
|
3050
|
+
}], placeholder: [{
|
|
3051
|
+
type: Input
|
|
3052
|
+
}], name: [{
|
|
3053
|
+
type: Input
|
|
3054
|
+
}], data: [{
|
|
3055
|
+
type: Input
|
|
3056
|
+
}], required: [{
|
|
3057
|
+
type: Input
|
|
3058
|
+
}], fullWidth: [{
|
|
3059
|
+
type: Input
|
|
3060
|
+
}], disabled: [{
|
|
3061
|
+
type: Input
|
|
3062
|
+
}], multiple: [{
|
|
3063
|
+
type: Input,
|
|
3064
|
+
args: [{ transform: booleanAttribute }]
|
|
3065
|
+
}], includeBlankOption: [{
|
|
3066
|
+
type: Input
|
|
3067
|
+
}], blankOptionLabel: [{
|
|
3068
|
+
type: Input
|
|
3069
|
+
}], emptyStateLabel: [{
|
|
3070
|
+
type: Input
|
|
3071
|
+
}], searchable: [{
|
|
3072
|
+
type: Input
|
|
3073
|
+
}], remoteSearch: [{
|
|
3074
|
+
type: Input
|
|
3075
|
+
}], searchPlaceholder: [{
|
|
3076
|
+
type: Input
|
|
3077
|
+
}], options: [{
|
|
3078
|
+
type: Input
|
|
3079
|
+
}], error: [{
|
|
3080
|
+
type: Input
|
|
3081
|
+
}], hint: [{
|
|
3082
|
+
type: Input
|
|
3083
|
+
}], triggerClass: [{
|
|
3084
|
+
type: Input
|
|
3085
|
+
}], panelClass: [{
|
|
3086
|
+
type: Input
|
|
3087
|
+
}], valueChange: [{
|
|
3088
|
+
type: Output
|
|
3089
|
+
}], searchChange: [{
|
|
3090
|
+
type: Output
|
|
3091
|
+
}], dropdownTrigger: [{
|
|
3092
|
+
type: ViewChild,
|
|
3093
|
+
args: ['dropdownTrigger', { static: true, read: ElementRef }]
|
|
3094
|
+
}] } });
|
|
3095
|
+
|
|
3096
|
+
class UiSlugInputComponent {
|
|
3097
|
+
label;
|
|
3098
|
+
hint;
|
|
3099
|
+
placeholder;
|
|
3100
|
+
name;
|
|
3101
|
+
id;
|
|
3102
|
+
required = false;
|
|
3103
|
+
fullWidth = false;
|
|
3104
|
+
autocomplete = 'off';
|
|
3105
|
+
readonly = false;
|
|
3106
|
+
error;
|
|
3107
|
+
invalidInput = new EventEmitter();
|
|
3108
|
+
disabled = false;
|
|
3109
|
+
value = '';
|
|
3110
|
+
static nextId = 0;
|
|
3111
|
+
generatedId = `ui-slug-input-${UiSlugInputComponent.nextId++}`;
|
|
3112
|
+
onChange = () => undefined;
|
|
3113
|
+
onTouched = () => undefined;
|
|
3114
|
+
get controlId() {
|
|
3115
|
+
return this.id ?? this.generatedId;
|
|
3116
|
+
}
|
|
3117
|
+
writeValue(value) {
|
|
3118
|
+
this.value = value ?? '';
|
|
3119
|
+
}
|
|
3120
|
+
registerOnChange(fn) {
|
|
3121
|
+
this.onChange = fn;
|
|
3122
|
+
}
|
|
3123
|
+
registerOnTouched(fn) {
|
|
3124
|
+
this.onTouched = fn;
|
|
3125
|
+
}
|
|
3126
|
+
setDisabledState(isDisabled) {
|
|
3127
|
+
this.disabled = isDisabled;
|
|
3128
|
+
}
|
|
3129
|
+
onValueChange(target) {
|
|
3130
|
+
const normalized = this.normalizeSlug(target.value);
|
|
3131
|
+
if (target.value !== normalized) {
|
|
3132
|
+
target.value = normalized;
|
|
3133
|
+
this.invalidInput.emit();
|
|
3134
|
+
}
|
|
3135
|
+
this.value = normalized;
|
|
3136
|
+
this.onChange(normalized);
|
|
3137
|
+
}
|
|
3138
|
+
normalizeSlug(value) {
|
|
3139
|
+
return (value ?? '')
|
|
3140
|
+
.toLowerCase()
|
|
3141
|
+
.replace(/\s+/g, '_')
|
|
3142
|
+
.replace(/[^a-z0-9_]/g, '');
|
|
3143
|
+
}
|
|
3144
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSlugInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3145
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiSlugInputComponent, isStandalone: true, selector: "forms-slug-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", error: "error" }, outputs: { invalidInput: "invalidInput" }, providers: [
|
|
3146
|
+
{
|
|
3147
|
+
provide: NG_VALUE_ACCESSOR,
|
|
3148
|
+
useExisting: forwardRef(() => UiSlugInputComponent),
|
|
3149
|
+
multi: true,
|
|
3150
|
+
},
|
|
3151
|
+
], 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 [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3152
|
+
}
|
|
3153
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSlugInputComponent, decorators: [{
|
|
3154
|
+
type: Component,
|
|
3155
|
+
args: [{ selector: 'forms-slug-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
3156
|
+
{
|
|
3157
|
+
provide: NG_VALUE_ACCESSOR,
|
|
3158
|
+
useExisting: forwardRef(() => UiSlugInputComponent),
|
|
3159
|
+
multi: true,
|
|
3160
|
+
},
|
|
3161
|
+
], 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 [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\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\n" }]
|
|
3162
|
+
}], propDecorators: { label: [{
|
|
3163
|
+
type: Input
|
|
3164
|
+
}], hint: [{
|
|
3165
|
+
type: Input
|
|
3166
|
+
}], placeholder: [{
|
|
3167
|
+
type: Input
|
|
3168
|
+
}], name: [{
|
|
3169
|
+
type: Input
|
|
3170
|
+
}], id: [{
|
|
3171
|
+
type: Input
|
|
3172
|
+
}], required: [{
|
|
3173
|
+
type: Input
|
|
3174
|
+
}], fullWidth: [{
|
|
3175
|
+
type: Input
|
|
3176
|
+
}], autocomplete: [{
|
|
3177
|
+
type: Input
|
|
3178
|
+
}], readonly: [{
|
|
3179
|
+
type: Input
|
|
3180
|
+
}], error: [{
|
|
3181
|
+
type: Input
|
|
3182
|
+
}], invalidInput: [{
|
|
3183
|
+
type: Output
|
|
3184
|
+
}] } });
|
|
3185
|
+
|
|
3186
|
+
/**
|
|
3187
|
+
* Generated bundle index. Do not edit.
|
|
3188
|
+
*/
|
|
3189
|
+
|
|
3190
|
+
export { MaroFormsApiService, UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS, UI_FORM_INPUT_REGISTRY, UiActionFormModalComponent, UiBarcodeScannerInputComponent, UiCheckboxInputComponent, UiColorInputComponent, UiDateInputComponent, UiDatetimeInputComponent, UiDynamicFormComponent, UiEmailInputComponent, UiFileInputComponent, UiFormContext, UiFormInputHostComponent, UiFormLookupService, UiInputComponent, UiNumberInputComponent, UiPasswordInputComponent, UiPhotoGalleryFieldComponent, UiPhotoInputComponent, UiRangeSliderComponent, UiSelectComponent, UiSlugInputComponent, UiTextInputComponent, UiTextareaInputComponent, flattenDynamicFormFields, isDynamicFormContainerField, isDynamicFormTemplateField };
|
|
3191
|
+
//# sourceMappingURL=maro-tech-form.mjs.map
|