ngx-dynamic-entity 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/fesm2022/ngx-dynamic-entity.mjs +1553 -9
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/package.json +2 -2
- package/styles.css +210 -0
- package/types/ngx-dynamic-entity.d.ts +268 -4
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, ElementRef, computed, afterNextRender, HostListener, ViewChildren, Output } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/forms';
|
|
4
4
|
import { ReactiveFormsModule, Validators, FormBuilder, FormGroup, FormArray, FormsModule } from '@angular/forms';
|
|
5
|
-
import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, getTabPath, fieldRefFor, toRefToken, refOf, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, collectFieldScopes, applyAutoPatch, isUnsafePath, fieldsUnderTab, valuesEqual, parseCsv, deriveImportColumns, suggestMapping, applyMapping, toCsv, stripIndices, CORE_VERSION, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } from '@dynamic-entity/core';
|
|
5
|
+
import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, isValidUrl, isValidPhone, fileRefName, sliderBounds, ratingScale, normalizeHexColor, normalizeTags, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, getTabPath, fieldRefFor, toRefToken, refOf, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, collectFieldScopes, applyAutoPatch, isUnsafePath, fieldsUnderTab, valuesEqual, parseCsv, deriveImportColumns, suggestMapping, applyMapping, toCsv, stripIndices, CORE_VERSION, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } from '@dynamic-entity/core';
|
|
6
6
|
export { formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
|
|
7
7
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
8
8
|
import { Subject, isObservable, firstValueFrom } from 'rxjs';
|
|
@@ -302,6 +302,10 @@ const DEFAULT_VALIDATION_MESSAGES = {
|
|
|
302
302
|
required: 'This field is required.',
|
|
303
303
|
requiredSelection: 'Please select an option.',
|
|
304
304
|
email: 'Please enter a valid email address.',
|
|
305
|
+
url: 'Please enter a full web address, starting with https://.',
|
|
306
|
+
phone: 'Please enter a valid phone number.',
|
|
307
|
+
minItems: (_lang, err) => `At least ${err?.requiredLength} required.`,
|
|
308
|
+
maxItems: (_lang, err) => `No more than ${err?.requiredLength} allowed.`,
|
|
305
309
|
pattern: 'Invalid format.',
|
|
306
310
|
minlength: (_lang, err) => `Minimum ${err?.requiredLength} characters required.`,
|
|
307
311
|
maxlength: (_lang, err) => `Maximum ${err?.requiredLength} characters allowed.`,
|
|
@@ -318,18 +322,35 @@ const DEFAULT_VALIDATION_MESSAGES = {
|
|
|
318
322
|
* first error actually on the control — so the shared list costs nothing and spares every
|
|
319
323
|
* field component a near-identical copy of it.
|
|
320
324
|
*/
|
|
321
|
-
const COMMON_ORDER = [
|
|
325
|
+
const COMMON_ORDER = [
|
|
326
|
+
'required',
|
|
327
|
+
'email',
|
|
328
|
+
'url',
|
|
329
|
+
'phone',
|
|
330
|
+
'min',
|
|
331
|
+
'max',
|
|
332
|
+
'minlength',
|
|
333
|
+
'maxlength',
|
|
334
|
+
'pattern',
|
|
335
|
+
];
|
|
322
336
|
/**
|
|
323
337
|
* Where a field type disagrees with the common list.
|
|
324
338
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
339
|
+
* Each for a reason about how the control reads rather than what it can raise: a select says
|
|
340
|
+
* "Please select an option" where an input says "This field is required"; a number that is
|
|
341
|
+
* neither required nor out of range is not "invalid", it is not a number; a control with no
|
|
342
|
+
* text in it cannot fail a length or format rule; and a `tags` list is counted in items, not
|
|
343
|
+
* characters, although Angular reports both as `minlength`.
|
|
329
344
|
*/
|
|
330
345
|
const ORDER_BY_FIELD_TYPE = {
|
|
331
346
|
dropdown: { order: [['required', 'requiredSelection']], fallback: 'invalidSelection' },
|
|
332
347
|
number: { order: ['required', 'min', 'max'], fallback: 'invalidNumber' },
|
|
348
|
+
slider: { order: ['required', 'min', 'max'], fallback: 'invalid' },
|
|
349
|
+
rating: { order: ['required', 'min', 'max'], fallback: 'invalid' },
|
|
350
|
+
tags: {
|
|
351
|
+
order: ['required', ['minlength', 'minItems'], ['maxlength', 'maxItems'], 'pattern'],
|
|
352
|
+
fallback: 'invalid',
|
|
353
|
+
},
|
|
333
354
|
boolean: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
334
355
|
checkbox: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
335
356
|
file: { order: ['required', 'pattern'], fallback: 'invalid' },
|
|
@@ -854,6 +875,11 @@ const DEFAULT_UI_TEXT = {
|
|
|
854
875
|
noImage: 'No image',
|
|
855
876
|
uploadImage: 'Upload',
|
|
856
877
|
changeImage: 'Change',
|
|
878
|
+
// Rating, colour and tags
|
|
879
|
+
ratingValue: '{value} of {max}',
|
|
880
|
+
clearValue: 'Clear {field}',
|
|
881
|
+
addTagPlaceholder: 'Type and press Enter',
|
|
882
|
+
removeTag: 'Remove {tag}',
|
|
857
883
|
// Month / year pickers
|
|
858
884
|
month: 'Month',
|
|
859
885
|
year: 'Year',
|
|
@@ -5195,6 +5221,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
5195
5221
|
type: Input
|
|
5196
5222
|
}] } });
|
|
5197
5223
|
|
|
5224
|
+
/**
|
|
5225
|
+
* A format check that, like `Validators.email`, passes an empty value.
|
|
5226
|
+
*
|
|
5227
|
+
* Emptiness is `required`'s question. Answering it here too would give an optional field an
|
|
5228
|
+
* error the moment it was cleared.
|
|
5229
|
+
*/
|
|
5230
|
+
function formatValidator(key, test) {
|
|
5231
|
+
return (control) => {
|
|
5232
|
+
const value = control.value;
|
|
5233
|
+
if (value === null || value === undefined || value === '')
|
|
5234
|
+
return null;
|
|
5235
|
+
return test(String(value)) ? null : { [key]: true };
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
/** The built-in web address check. The rule itself is in core, so an import applies the same one. */
|
|
5239
|
+
const urlValidator = formatValidator('url', isValidUrl);
|
|
5240
|
+
/** The built-in phone number check. The rule itself is in core, so an import applies the same one. */
|
|
5241
|
+
const phoneValidator = formatValidator('phone', isValidPhone);
|
|
5198
5242
|
/**
|
|
5199
5243
|
* ValidatorRegistryService — resolves validator configs / key strings to Angular ValidatorFns.
|
|
5200
5244
|
*/
|
|
@@ -5213,6 +5257,10 @@ class ValidatorRegistryService {
|
|
|
5213
5257
|
return Validators.required;
|
|
5214
5258
|
if (validatorKey === 'email')
|
|
5215
5259
|
return Validators.email;
|
|
5260
|
+
if (validatorKey === 'url')
|
|
5261
|
+
return urlValidator;
|
|
5262
|
+
if (validatorKey === 'phone')
|
|
5263
|
+
return phoneValidator;
|
|
5216
5264
|
const [name, param] = validatorKey.split(':');
|
|
5217
5265
|
const value = parseFloat(param);
|
|
5218
5266
|
if (name === 'min' && !isNaN(value))
|
|
@@ -5262,6 +5310,10 @@ class ValidatorRegistryService {
|
|
|
5262
5310
|
}
|
|
5263
5311
|
if (config.email)
|
|
5264
5312
|
fnList.push(Validators.email);
|
|
5313
|
+
if (config.url)
|
|
5314
|
+
fnList.push(urlValidator);
|
|
5315
|
+
if (config.phone)
|
|
5316
|
+
fnList.push(phoneValidator);
|
|
5265
5317
|
// Named validators from the consumer registry, so a registered validator can be named
|
|
5266
5318
|
// from a typed schema.
|
|
5267
5319
|
for (const key of config.custom ?? []) {
|
|
@@ -6151,12 +6203,1492 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
6151
6203
|
type: Input
|
|
6152
6204
|
}] } });
|
|
6153
6205
|
|
|
6206
|
+
/**
|
|
6207
|
+
* URL field: a `type="url"` input, and a link when read-only.
|
|
6208
|
+
*
|
|
6209
|
+
* The read-only link is rendered only for a value that passes `isValidUrl`, meaning an http or
|
|
6210
|
+
* https address. A record can hold anything, including `javascript:`, and a field whose job is
|
|
6211
|
+
* to show an address must not turn a stored script into something to click. Anything else is
|
|
6212
|
+
* shown as plain text.
|
|
6213
|
+
*/
|
|
6214
|
+
class UrlFieldComponent {
|
|
6215
|
+
/**
|
|
6216
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6217
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6218
|
+
*/
|
|
6219
|
+
instanceId = nextFieldInstanceId();
|
|
6220
|
+
domId(suffix = '') {
|
|
6221
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6222
|
+
}
|
|
6223
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6224
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6225
|
+
messages = inject(ValidationMessagesService);
|
|
6226
|
+
field;
|
|
6227
|
+
control;
|
|
6228
|
+
language = 'en';
|
|
6229
|
+
readonly = false;
|
|
6230
|
+
masked = false;
|
|
6231
|
+
get label() {
|
|
6232
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6233
|
+
}
|
|
6234
|
+
get placeholder() {
|
|
6235
|
+
return resolveLabel(this.field?.placeholder, this.language);
|
|
6236
|
+
}
|
|
6237
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6238
|
+
get hint() {
|
|
6239
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6240
|
+
}
|
|
6241
|
+
/** Whether the stored value is an http(s) address, and so safe to render as a link. */
|
|
6242
|
+
get linkable() {
|
|
6243
|
+
const value = this.control?.value;
|
|
6244
|
+
return typeof value === 'string' && isValidUrl(value);
|
|
6245
|
+
}
|
|
6246
|
+
/** The ids of whatever is describing this control right now. */
|
|
6247
|
+
describedBy() {
|
|
6248
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6249
|
+
}
|
|
6250
|
+
get errorMessage() {
|
|
6251
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6252
|
+
return '';
|
|
6253
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6254
|
+
}
|
|
6255
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UrlFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6256
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: UrlFieldComponent, isStandalone: true, selector: "ngx-url-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
6257
|
+
<div
|
|
6258
|
+
class="ngx-field ngx-field--url"
|
|
6259
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6260
|
+
[attr.data-field-type]="field.type"
|
|
6261
|
+
[class.ngx-field--readonly]="readonly"
|
|
6262
|
+
[class.ngx-field--masked]="masked"
|
|
6263
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6264
|
+
>
|
|
6265
|
+
<div class="ngx-field__label-row">
|
|
6266
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6267
|
+
{{ label }}
|
|
6268
|
+
@if (field.validators?.required) {
|
|
6269
|
+
<span class="ngx-field__req">*</span>
|
|
6270
|
+
}
|
|
6271
|
+
</label>
|
|
6272
|
+
@if (hint) {
|
|
6273
|
+
<span class="ngx-field__hint-wrap">
|
|
6274
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6275
|
+
<span
|
|
6276
|
+
class="ngx-field__hint"
|
|
6277
|
+
role="tooltip"
|
|
6278
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6279
|
+
[id]="domId('-hint')"
|
|
6280
|
+
>{{ hint }}</span
|
|
6281
|
+
>
|
|
6282
|
+
</span>
|
|
6283
|
+
}
|
|
6284
|
+
</div>
|
|
6285
|
+
@if (masked) {
|
|
6286
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6287
|
+
maskedText
|
|
6288
|
+
}}</span>
|
|
6289
|
+
} @else if (readonly) {
|
|
6290
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6291
|
+
@if (linkable) {
|
|
6292
|
+
<a [href]="control.value" class="ngx-field__link" target="_blank" rel="noopener noreferrer">{{
|
|
6293
|
+
control.value
|
|
6294
|
+
}}</a>
|
|
6295
|
+
} @else {
|
|
6296
|
+
{{ control.value || '—' }}
|
|
6297
|
+
}
|
|
6298
|
+
</span>
|
|
6299
|
+
} @else {
|
|
6300
|
+
<input
|
|
6301
|
+
[id]="domId()"
|
|
6302
|
+
class="ngx-field__input"
|
|
6303
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6304
|
+
type="url"
|
|
6305
|
+
inputmode="url"
|
|
6306
|
+
autocomplete="url"
|
|
6307
|
+
[formControl]="$any(control)"
|
|
6308
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6309
|
+
[attr.aria-describedby]="describedBy()"
|
|
6310
|
+
[placeholder]="placeholder || 'https://example.com'"
|
|
6311
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6312
|
+
/>
|
|
6313
|
+
@if (errorMessage) {
|
|
6314
|
+
<span
|
|
6315
|
+
class="ngx-field__error"
|
|
6316
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6317
|
+
[id]="domId('-error')"
|
|
6318
|
+
role="alert"
|
|
6319
|
+
>{{ errorMessage }}</span
|
|
6320
|
+
>
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
</div>
|
|
6324
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6325
|
+
}
|
|
6326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UrlFieldComponent, decorators: [{
|
|
6327
|
+
type: Component,
|
|
6328
|
+
args: [{
|
|
6329
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6330
|
+
selector: 'ngx-url-field',
|
|
6331
|
+
standalone: true,
|
|
6332
|
+
imports: [ReactiveFormsModule],
|
|
6333
|
+
template: `
|
|
6334
|
+
<div
|
|
6335
|
+
class="ngx-field ngx-field--url"
|
|
6336
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6337
|
+
[attr.data-field-type]="field.type"
|
|
6338
|
+
[class.ngx-field--readonly]="readonly"
|
|
6339
|
+
[class.ngx-field--masked]="masked"
|
|
6340
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6341
|
+
>
|
|
6342
|
+
<div class="ngx-field__label-row">
|
|
6343
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6344
|
+
{{ label }}
|
|
6345
|
+
@if (field.validators?.required) {
|
|
6346
|
+
<span class="ngx-field__req">*</span>
|
|
6347
|
+
}
|
|
6348
|
+
</label>
|
|
6349
|
+
@if (hint) {
|
|
6350
|
+
<span class="ngx-field__hint-wrap">
|
|
6351
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6352
|
+
<span
|
|
6353
|
+
class="ngx-field__hint"
|
|
6354
|
+
role="tooltip"
|
|
6355
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6356
|
+
[id]="domId('-hint')"
|
|
6357
|
+
>{{ hint }}</span
|
|
6358
|
+
>
|
|
6359
|
+
</span>
|
|
6360
|
+
}
|
|
6361
|
+
</div>
|
|
6362
|
+
@if (masked) {
|
|
6363
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6364
|
+
maskedText
|
|
6365
|
+
}}</span>
|
|
6366
|
+
} @else if (readonly) {
|
|
6367
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6368
|
+
@if (linkable) {
|
|
6369
|
+
<a [href]="control.value" class="ngx-field__link" target="_blank" rel="noopener noreferrer">{{
|
|
6370
|
+
control.value
|
|
6371
|
+
}}</a>
|
|
6372
|
+
} @else {
|
|
6373
|
+
{{ control.value || '—' }}
|
|
6374
|
+
}
|
|
6375
|
+
</span>
|
|
6376
|
+
} @else {
|
|
6377
|
+
<input
|
|
6378
|
+
[id]="domId()"
|
|
6379
|
+
class="ngx-field__input"
|
|
6380
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6381
|
+
type="url"
|
|
6382
|
+
inputmode="url"
|
|
6383
|
+
autocomplete="url"
|
|
6384
|
+
[formControl]="$any(control)"
|
|
6385
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6386
|
+
[attr.aria-describedby]="describedBy()"
|
|
6387
|
+
[placeholder]="placeholder || 'https://example.com'"
|
|
6388
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6389
|
+
/>
|
|
6390
|
+
@if (errorMessage) {
|
|
6391
|
+
<span
|
|
6392
|
+
class="ngx-field__error"
|
|
6393
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6394
|
+
[id]="domId('-error')"
|
|
6395
|
+
role="alert"
|
|
6396
|
+
>{{ errorMessage }}</span
|
|
6397
|
+
>
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
</div>
|
|
6401
|
+
`,
|
|
6402
|
+
}]
|
|
6403
|
+
}], propDecorators: { field: [{
|
|
6404
|
+
type: Input
|
|
6405
|
+
}], control: [{
|
|
6406
|
+
type: Input
|
|
6407
|
+
}], language: [{
|
|
6408
|
+
type: Input
|
|
6409
|
+
}], readonly: [{
|
|
6410
|
+
type: Input
|
|
6411
|
+
}], masked: [{
|
|
6412
|
+
type: Input
|
|
6413
|
+
}] } });
|
|
6414
|
+
|
|
6415
|
+
/**
|
|
6416
|
+
* Phone field: a `type="tel"` input, and a `tel:` link when read-only.
|
|
6417
|
+
*
|
|
6418
|
+
* The value is stored exactly as typed, spaces and brackets included. The number is shown back
|
|
6419
|
+
* to people far more often than it is dialled, and `+44 20 7946 0958` is easier to read than
|
|
6420
|
+
* its digits run together. Only the link target is stripped down to what a dialler needs.
|
|
6421
|
+
*/
|
|
6422
|
+
class PhoneFieldComponent {
|
|
6423
|
+
/**
|
|
6424
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6425
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6426
|
+
*/
|
|
6427
|
+
instanceId = nextFieldInstanceId();
|
|
6428
|
+
domId(suffix = '') {
|
|
6429
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6430
|
+
}
|
|
6431
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6432
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6433
|
+
messages = inject(ValidationMessagesService);
|
|
6434
|
+
field;
|
|
6435
|
+
control;
|
|
6436
|
+
language = 'en';
|
|
6437
|
+
readonly = false;
|
|
6438
|
+
masked = false;
|
|
6439
|
+
get label() {
|
|
6440
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6441
|
+
}
|
|
6442
|
+
get placeholder() {
|
|
6443
|
+
return resolveLabel(this.field?.placeholder, this.language);
|
|
6444
|
+
}
|
|
6445
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6446
|
+
get hint() {
|
|
6447
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6448
|
+
}
|
|
6449
|
+
/** `tel:` plus the digits and a leading `+`, or null for a value that is not a phone number. */
|
|
6450
|
+
get dialHref() {
|
|
6451
|
+
const value = this.control?.value;
|
|
6452
|
+
if (typeof value !== 'string' || !isValidPhone(value))
|
|
6453
|
+
return null;
|
|
6454
|
+
return `tel:${value.trim().startsWith('+') ? '+' : ''}${value.replace(/\D/g, '')}`;
|
|
6455
|
+
}
|
|
6456
|
+
/** The ids of whatever is describing this control right now. */
|
|
6457
|
+
describedBy() {
|
|
6458
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6459
|
+
}
|
|
6460
|
+
get errorMessage() {
|
|
6461
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6462
|
+
return '';
|
|
6463
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6464
|
+
}
|
|
6465
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PhoneFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6466
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: PhoneFieldComponent, isStandalone: true, selector: "ngx-phone-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
6467
|
+
<div
|
|
6468
|
+
class="ngx-field ngx-field--phone"
|
|
6469
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6470
|
+
[attr.data-field-type]="field.type"
|
|
6471
|
+
[class.ngx-field--readonly]="readonly"
|
|
6472
|
+
[class.ngx-field--masked]="masked"
|
|
6473
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6474
|
+
>
|
|
6475
|
+
<div class="ngx-field__label-row">
|
|
6476
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6477
|
+
{{ label }}
|
|
6478
|
+
@if (field.validators?.required) {
|
|
6479
|
+
<span class="ngx-field__req">*</span>
|
|
6480
|
+
}
|
|
6481
|
+
</label>
|
|
6482
|
+
@if (hint) {
|
|
6483
|
+
<span class="ngx-field__hint-wrap">
|
|
6484
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6485
|
+
<span
|
|
6486
|
+
class="ngx-field__hint"
|
|
6487
|
+
role="tooltip"
|
|
6488
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6489
|
+
[id]="domId('-hint')"
|
|
6490
|
+
>{{ hint }}</span
|
|
6491
|
+
>
|
|
6492
|
+
</span>
|
|
6493
|
+
}
|
|
6494
|
+
</div>
|
|
6495
|
+
@if (masked) {
|
|
6496
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6497
|
+
maskedText
|
|
6498
|
+
}}</span>
|
|
6499
|
+
} @else if (readonly) {
|
|
6500
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6501
|
+
@if (dialHref) {
|
|
6502
|
+
<a [href]="dialHref" class="ngx-field__link">{{ control.value }}</a>
|
|
6503
|
+
} @else {
|
|
6504
|
+
{{ control.value || '—' }}
|
|
6505
|
+
}
|
|
6506
|
+
</span>
|
|
6507
|
+
} @else {
|
|
6508
|
+
<input
|
|
6509
|
+
[id]="domId()"
|
|
6510
|
+
class="ngx-field__input"
|
|
6511
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6512
|
+
type="tel"
|
|
6513
|
+
inputmode="tel"
|
|
6514
|
+
autocomplete="tel"
|
|
6515
|
+
[formControl]="$any(control)"
|
|
6516
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6517
|
+
[attr.aria-describedby]="describedBy()"
|
|
6518
|
+
[placeholder]="placeholder"
|
|
6519
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6520
|
+
/>
|
|
6521
|
+
@if (errorMessage) {
|
|
6522
|
+
<span
|
|
6523
|
+
class="ngx-field__error"
|
|
6524
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6525
|
+
[id]="domId('-error')"
|
|
6526
|
+
role="alert"
|
|
6527
|
+
>{{ errorMessage }}</span
|
|
6528
|
+
>
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
</div>
|
|
6532
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6533
|
+
}
|
|
6534
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PhoneFieldComponent, decorators: [{
|
|
6535
|
+
type: Component,
|
|
6536
|
+
args: [{
|
|
6537
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6538
|
+
selector: 'ngx-phone-field',
|
|
6539
|
+
standalone: true,
|
|
6540
|
+
imports: [ReactiveFormsModule],
|
|
6541
|
+
template: `
|
|
6542
|
+
<div
|
|
6543
|
+
class="ngx-field ngx-field--phone"
|
|
6544
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6545
|
+
[attr.data-field-type]="field.type"
|
|
6546
|
+
[class.ngx-field--readonly]="readonly"
|
|
6547
|
+
[class.ngx-field--masked]="masked"
|
|
6548
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6549
|
+
>
|
|
6550
|
+
<div class="ngx-field__label-row">
|
|
6551
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6552
|
+
{{ label }}
|
|
6553
|
+
@if (field.validators?.required) {
|
|
6554
|
+
<span class="ngx-field__req">*</span>
|
|
6555
|
+
}
|
|
6556
|
+
</label>
|
|
6557
|
+
@if (hint) {
|
|
6558
|
+
<span class="ngx-field__hint-wrap">
|
|
6559
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6560
|
+
<span
|
|
6561
|
+
class="ngx-field__hint"
|
|
6562
|
+
role="tooltip"
|
|
6563
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6564
|
+
[id]="domId('-hint')"
|
|
6565
|
+
>{{ hint }}</span
|
|
6566
|
+
>
|
|
6567
|
+
</span>
|
|
6568
|
+
}
|
|
6569
|
+
</div>
|
|
6570
|
+
@if (masked) {
|
|
6571
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6572
|
+
maskedText
|
|
6573
|
+
}}</span>
|
|
6574
|
+
} @else if (readonly) {
|
|
6575
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6576
|
+
@if (dialHref) {
|
|
6577
|
+
<a [href]="dialHref" class="ngx-field__link">{{ control.value }}</a>
|
|
6578
|
+
} @else {
|
|
6579
|
+
{{ control.value || '—' }}
|
|
6580
|
+
}
|
|
6581
|
+
</span>
|
|
6582
|
+
} @else {
|
|
6583
|
+
<input
|
|
6584
|
+
[id]="domId()"
|
|
6585
|
+
class="ngx-field__input"
|
|
6586
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6587
|
+
type="tel"
|
|
6588
|
+
inputmode="tel"
|
|
6589
|
+
autocomplete="tel"
|
|
6590
|
+
[formControl]="$any(control)"
|
|
6591
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6592
|
+
[attr.aria-describedby]="describedBy()"
|
|
6593
|
+
[placeholder]="placeholder"
|
|
6594
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6595
|
+
/>
|
|
6596
|
+
@if (errorMessage) {
|
|
6597
|
+
<span
|
|
6598
|
+
class="ngx-field__error"
|
|
6599
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6600
|
+
[id]="domId('-error')"
|
|
6601
|
+
role="alert"
|
|
6602
|
+
>{{ errorMessage }}</span
|
|
6603
|
+
>
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
</div>
|
|
6607
|
+
`,
|
|
6608
|
+
}]
|
|
6609
|
+
}], propDecorators: { field: [{
|
|
6610
|
+
type: Input
|
|
6611
|
+
}], control: [{
|
|
6612
|
+
type: Input
|
|
6613
|
+
}], language: [{
|
|
6614
|
+
type: Input
|
|
6615
|
+
}], readonly: [{
|
|
6616
|
+
type: Input
|
|
6617
|
+
}], masked: [{
|
|
6618
|
+
type: Input
|
|
6619
|
+
}] } });
|
|
6620
|
+
|
|
6621
|
+
/**
|
|
6622
|
+
* Slider field: a range input over `validators.min`–`validators.max` in steps of `field.step`.
|
|
6623
|
+
*
|
|
6624
|
+
* A range input cannot show that it is empty. With no value it puts the thumb at the midpoint,
|
|
6625
|
+
* which looks exactly like a chosen value. The readout beside it is therefore the value itself,
|
|
6626
|
+
* and shows a dash until someone moves the thumb. Without it, a required slider nobody touched
|
|
6627
|
+
* would look answered and still fail at save.
|
|
6628
|
+
*/
|
|
6629
|
+
class SliderFieldComponent {
|
|
6630
|
+
/**
|
|
6631
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6632
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6633
|
+
*/
|
|
6634
|
+
instanceId = nextFieldInstanceId();
|
|
6635
|
+
domId(suffix = '') {
|
|
6636
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6637
|
+
}
|
|
6638
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6639
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6640
|
+
messages = inject(ValidationMessagesService);
|
|
6641
|
+
field;
|
|
6642
|
+
control;
|
|
6643
|
+
language = 'en';
|
|
6644
|
+
readonly = false;
|
|
6645
|
+
masked = false;
|
|
6646
|
+
get label() {
|
|
6647
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6648
|
+
}
|
|
6649
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6650
|
+
get hint() {
|
|
6651
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6652
|
+
}
|
|
6653
|
+
get bounds() {
|
|
6654
|
+
return sliderBounds(this.field);
|
|
6655
|
+
}
|
|
6656
|
+
/** Whether the control holds a number. The thumb's position cannot say. */
|
|
6657
|
+
get hasValue() {
|
|
6658
|
+
const value = this.control?.value;
|
|
6659
|
+
return value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value));
|
|
6660
|
+
}
|
|
6661
|
+
get display() {
|
|
6662
|
+
return this.hasValue ? String(this.control.value) : '—';
|
|
6663
|
+
}
|
|
6664
|
+
/** The ids of whatever is describing this control right now. */
|
|
6665
|
+
describedBy() {
|
|
6666
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6667
|
+
}
|
|
6668
|
+
get errorMessage() {
|
|
6669
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6670
|
+
return '';
|
|
6671
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6672
|
+
}
|
|
6673
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6674
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SliderFieldComponent, isStandalone: true, selector: "ngx-slider-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
6675
|
+
<div
|
|
6676
|
+
class="ngx-field ngx-field--slider"
|
|
6677
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6678
|
+
[attr.data-field-type]="field.type"
|
|
6679
|
+
[class.ngx-field--readonly]="readonly"
|
|
6680
|
+
[class.ngx-field--masked]="masked"
|
|
6681
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6682
|
+
>
|
|
6683
|
+
<div class="ngx-field__label-row">
|
|
6684
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6685
|
+
{{ label }}
|
|
6686
|
+
@if (field.validators?.required) {
|
|
6687
|
+
<span class="ngx-field__req">*</span>
|
|
6688
|
+
}
|
|
6689
|
+
</label>
|
|
6690
|
+
@if (hint) {
|
|
6691
|
+
<span class="ngx-field__hint-wrap">
|
|
6692
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6693
|
+
<span
|
|
6694
|
+
class="ngx-field__hint"
|
|
6695
|
+
role="tooltip"
|
|
6696
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6697
|
+
[id]="domId('-hint')"
|
|
6698
|
+
>{{ hint }}</span
|
|
6699
|
+
>
|
|
6700
|
+
</span>
|
|
6701
|
+
}
|
|
6702
|
+
</div>
|
|
6703
|
+
@if (masked) {
|
|
6704
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6705
|
+
maskedText
|
|
6706
|
+
}}</span>
|
|
6707
|
+
} @else if (readonly) {
|
|
6708
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ display }}</span>
|
|
6709
|
+
} @else {
|
|
6710
|
+
<div class="ngx-field__slider-wrap">
|
|
6711
|
+
<input
|
|
6712
|
+
[id]="domId()"
|
|
6713
|
+
type="range"
|
|
6714
|
+
class="ngx-field__slider"
|
|
6715
|
+
[class.ngx-field__slider--unset]="!hasValue"
|
|
6716
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6717
|
+
[min]="bounds.min"
|
|
6718
|
+
[max]="bounds.max"
|
|
6719
|
+
[step]="bounds.step"
|
|
6720
|
+
[formControl]="$any(control)"
|
|
6721
|
+
[attr.aria-valuetext]="hasValue ? null : display"
|
|
6722
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6723
|
+
[attr.aria-describedby]="describedBy()"
|
|
6724
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6725
|
+
/>
|
|
6726
|
+
<output
|
|
6727
|
+
class="ngx-field__slider-value"
|
|
6728
|
+
[attr.for]="domId()"
|
|
6729
|
+
[attr.data-testid]="'field-' + field.id + '-readout'"
|
|
6730
|
+
aria-hidden="true"
|
|
6731
|
+
>{{ display }}</output
|
|
6732
|
+
>
|
|
6733
|
+
</div>
|
|
6734
|
+
@if (errorMessage) {
|
|
6735
|
+
<span
|
|
6736
|
+
class="ngx-field__error"
|
|
6737
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6738
|
+
[id]="domId('-error')"
|
|
6739
|
+
role="alert"
|
|
6740
|
+
>{{ errorMessage }}</span
|
|
6741
|
+
>
|
|
6742
|
+
}
|
|
6743
|
+
}
|
|
6744
|
+
</div>
|
|
6745
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6746
|
+
}
|
|
6747
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SliderFieldComponent, decorators: [{
|
|
6748
|
+
type: Component,
|
|
6749
|
+
args: [{
|
|
6750
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6751
|
+
selector: 'ngx-slider-field',
|
|
6752
|
+
standalone: true,
|
|
6753
|
+
imports: [ReactiveFormsModule],
|
|
6754
|
+
template: `
|
|
6755
|
+
<div
|
|
6756
|
+
class="ngx-field ngx-field--slider"
|
|
6757
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6758
|
+
[attr.data-field-type]="field.type"
|
|
6759
|
+
[class.ngx-field--readonly]="readonly"
|
|
6760
|
+
[class.ngx-field--masked]="masked"
|
|
6761
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6762
|
+
>
|
|
6763
|
+
<div class="ngx-field__label-row">
|
|
6764
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
6765
|
+
{{ label }}
|
|
6766
|
+
@if (field.validators?.required) {
|
|
6767
|
+
<span class="ngx-field__req">*</span>
|
|
6768
|
+
}
|
|
6769
|
+
</label>
|
|
6770
|
+
@if (hint) {
|
|
6771
|
+
<span class="ngx-field__hint-wrap">
|
|
6772
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6773
|
+
<span
|
|
6774
|
+
class="ngx-field__hint"
|
|
6775
|
+
role="tooltip"
|
|
6776
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6777
|
+
[id]="domId('-hint')"
|
|
6778
|
+
>{{ hint }}</span
|
|
6779
|
+
>
|
|
6780
|
+
</span>
|
|
6781
|
+
}
|
|
6782
|
+
</div>
|
|
6783
|
+
@if (masked) {
|
|
6784
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6785
|
+
maskedText
|
|
6786
|
+
}}</span>
|
|
6787
|
+
} @else if (readonly) {
|
|
6788
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ display }}</span>
|
|
6789
|
+
} @else {
|
|
6790
|
+
<div class="ngx-field__slider-wrap">
|
|
6791
|
+
<input
|
|
6792
|
+
[id]="domId()"
|
|
6793
|
+
type="range"
|
|
6794
|
+
class="ngx-field__slider"
|
|
6795
|
+
[class.ngx-field__slider--unset]="!hasValue"
|
|
6796
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
6797
|
+
[min]="bounds.min"
|
|
6798
|
+
[max]="bounds.max"
|
|
6799
|
+
[step]="bounds.step"
|
|
6800
|
+
[formControl]="$any(control)"
|
|
6801
|
+
[attr.aria-valuetext]="hasValue ? null : display"
|
|
6802
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
6803
|
+
[attr.aria-describedby]="describedBy()"
|
|
6804
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6805
|
+
/>
|
|
6806
|
+
<output
|
|
6807
|
+
class="ngx-field__slider-value"
|
|
6808
|
+
[attr.for]="domId()"
|
|
6809
|
+
[attr.data-testid]="'field-' + field.id + '-readout'"
|
|
6810
|
+
aria-hidden="true"
|
|
6811
|
+
>{{ display }}</output
|
|
6812
|
+
>
|
|
6813
|
+
</div>
|
|
6814
|
+
@if (errorMessage) {
|
|
6815
|
+
<span
|
|
6816
|
+
class="ngx-field__error"
|
|
6817
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
6818
|
+
[id]="domId('-error')"
|
|
6819
|
+
role="alert"
|
|
6820
|
+
>{{ errorMessage }}</span
|
|
6821
|
+
>
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
</div>
|
|
6825
|
+
`,
|
|
6826
|
+
}]
|
|
6827
|
+
}], propDecorators: { field: [{
|
|
6828
|
+
type: Input
|
|
6829
|
+
}], control: [{
|
|
6830
|
+
type: Input
|
|
6831
|
+
}], language: [{
|
|
6832
|
+
type: Input
|
|
6833
|
+
}], readonly: [{
|
|
6834
|
+
type: Input
|
|
6835
|
+
}], masked: [{
|
|
6836
|
+
type: Input
|
|
6837
|
+
}] } });
|
|
6838
|
+
|
|
6839
|
+
/**
|
|
6840
|
+
* Rating field: 1 to `validators.max` stars (default 5), stored as a number.
|
|
6841
|
+
*
|
|
6842
|
+
* Built from native radio inputs in a fieldset, not clickable spans. That gives it the
|
|
6843
|
+
* behaviour a radio group already has: arrow keys move the choice, Tab enters and leaves the
|
|
6844
|
+
* group once, and a screen reader announces "3 of 5, radio button, 3 of 5". Each star's
|
|
6845
|
+
* label is text a host can translate (`ratingValue`), because a star glyph read aloud is
|
|
6846
|
+
* "black star".
|
|
6847
|
+
*/
|
|
6848
|
+
class RatingFieldComponent {
|
|
6849
|
+
/**
|
|
6850
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
6851
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
6852
|
+
*/
|
|
6853
|
+
instanceId = nextFieldInstanceId();
|
|
6854
|
+
domId(suffix = '') {
|
|
6855
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
6856
|
+
}
|
|
6857
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
6858
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
6859
|
+
messages = inject(ValidationMessagesService);
|
|
6860
|
+
cdr = inject(ChangeDetectorRef);
|
|
6861
|
+
ui = inject(UiTextService);
|
|
6862
|
+
field;
|
|
6863
|
+
language = 'en';
|
|
6864
|
+
readonly = false;
|
|
6865
|
+
masked = false;
|
|
6866
|
+
_control;
|
|
6867
|
+
_controlSub;
|
|
6868
|
+
/**
|
|
6869
|
+
* Subscribed because the radios are not bound through a form directive. A value patched in
|
|
6870
|
+
* from outside (a reset, an autoPatch) would otherwise leave an OnPush view showing the old
|
|
6871
|
+
* stars.
|
|
6872
|
+
*/
|
|
6873
|
+
set control(value) {
|
|
6874
|
+
this._control = value;
|
|
6875
|
+
this._controlSub?.unsubscribe();
|
|
6876
|
+
this._controlSub = this._control?.valueChanges?.subscribe(() => this.cdr.markForCheck());
|
|
6877
|
+
}
|
|
6878
|
+
get control() {
|
|
6879
|
+
return this._control;
|
|
6880
|
+
}
|
|
6881
|
+
ngOnDestroy() {
|
|
6882
|
+
this._controlSub?.unsubscribe();
|
|
6883
|
+
}
|
|
6884
|
+
get label() {
|
|
6885
|
+
return resolveLabel(this.field?.label, this.language);
|
|
6886
|
+
}
|
|
6887
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
6888
|
+
get hint() {
|
|
6889
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
6890
|
+
}
|
|
6891
|
+
get scale() {
|
|
6892
|
+
return ratingScale(this.field);
|
|
6893
|
+
}
|
|
6894
|
+
get stars() {
|
|
6895
|
+
return Array.from({ length: this.scale }, (_, i) => i + 1);
|
|
6896
|
+
}
|
|
6897
|
+
/** The stored rating, or null when there is none or it is not a star on this scale. */
|
|
6898
|
+
get current() {
|
|
6899
|
+
const value = Number(this.control?.value);
|
|
6900
|
+
const raw = this.control?.value;
|
|
6901
|
+
if (raw === null || raw === undefined || raw === '' || !Number.isInteger(value))
|
|
6902
|
+
return null;
|
|
6903
|
+
return value >= 1 && value <= this.scale ? value : null;
|
|
6904
|
+
}
|
|
6905
|
+
select(star) {
|
|
6906
|
+
this.control?.setValue(star);
|
|
6907
|
+
this.control?.markAsTouched();
|
|
6908
|
+
this.control?.markAsDirty();
|
|
6909
|
+
}
|
|
6910
|
+
clear() {
|
|
6911
|
+
this.control?.setValue(null);
|
|
6912
|
+
this.control?.markAsTouched();
|
|
6913
|
+
this.control?.markAsDirty();
|
|
6914
|
+
}
|
|
6915
|
+
/** The ids of whatever is describing this control right now. */
|
|
6916
|
+
describedBy() {
|
|
6917
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
6918
|
+
}
|
|
6919
|
+
get errorMessage() {
|
|
6920
|
+
if (!this.control?.errors || !this.control.touched)
|
|
6921
|
+
return '';
|
|
6922
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
6923
|
+
}
|
|
6924
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6925
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RatingFieldComponent, isStandalone: true, selector: "ngx-rating-field", inputs: { field: "field", language: "language", readonly: "readonly", masked: "masked", control: "control" }, ngImport: i0, template: `
|
|
6926
|
+
<div
|
|
6927
|
+
class="ngx-field ngx-field--rating"
|
|
6928
|
+
[attr.data-testid]="'field-' + field.id"
|
|
6929
|
+
[attr.data-field-type]="field.type"
|
|
6930
|
+
[class.ngx-field--readonly]="readonly"
|
|
6931
|
+
[class.ngx-field--masked]="masked"
|
|
6932
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
6933
|
+
>
|
|
6934
|
+
<fieldset class="ngx-field__fieldset">
|
|
6935
|
+
<legend class="ngx-field__label">
|
|
6936
|
+
{{ label }}
|
|
6937
|
+
@if (field.validators?.required) {
|
|
6938
|
+
<span class="ngx-field__req">*</span>
|
|
6939
|
+
}
|
|
6940
|
+
@if (hint) {
|
|
6941
|
+
<!-- Inside the legend: a fieldset allows one legend, and it must come first. -->
|
|
6942
|
+
<span class="ngx-field__hint-wrap">
|
|
6943
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
6944
|
+
<span
|
|
6945
|
+
class="ngx-field__hint"
|
|
6946
|
+
role="tooltip"
|
|
6947
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
6948
|
+
[id]="domId('-hint')"
|
|
6949
|
+
>{{ hint }}</span
|
|
6950
|
+
>
|
|
6951
|
+
</span>
|
|
6952
|
+
}
|
|
6953
|
+
</legend>
|
|
6954
|
+
@if (masked) {
|
|
6955
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
6956
|
+
maskedText
|
|
6957
|
+
}}</span>
|
|
6958
|
+
} @else if (readonly) {
|
|
6959
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
6960
|
+
@if (current) {
|
|
6961
|
+
<span class="ngx-field__stars" aria-hidden="true">
|
|
6962
|
+
@for (star of stars; track star) {
|
|
6963
|
+
<span class="ngx-field__star-glyph" [class.ngx-field__star-glyph--on]="star <= current">{{
|
|
6964
|
+
star <= current ? '★' : '☆'
|
|
6965
|
+
}}</span>
|
|
6966
|
+
}
|
|
6967
|
+
</span>
|
|
6968
|
+
{{ ui.text('ratingValue', language, { value: current, max: scale }) }}
|
|
6969
|
+
} @else {
|
|
6970
|
+
—
|
|
6971
|
+
}
|
|
6972
|
+
</span>
|
|
6973
|
+
} @else {
|
|
6974
|
+
<div class="ngx-field__rating">
|
|
6975
|
+
@for (star of stars; track star) {
|
|
6976
|
+
<label class="ngx-field__star" [class.ngx-field__star--on]="current !== null && star <= current">
|
|
6977
|
+
<input
|
|
6978
|
+
type="radio"
|
|
6979
|
+
class="ngx-field__star-input"
|
|
6980
|
+
[id]="domId('-' + star)"
|
|
6981
|
+
[name]="domId()"
|
|
6982
|
+
[value]="star"
|
|
6983
|
+
[checked]="current === star"
|
|
6984
|
+
[attr.data-testid]="'field-' + field.id + '-star-' + star"
|
|
6985
|
+
[attr.aria-describedby]="describedBy()"
|
|
6986
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
6987
|
+
(change)="select(star)"
|
|
6988
|
+
/>
|
|
6989
|
+
<!-- Filled and hollow, so the rating is readable without telling colours apart. -->
|
|
6990
|
+
<span class="ngx-field__star-glyph" aria-hidden="true">{{
|
|
6991
|
+
current !== null && star <= current ? '★' : '☆'
|
|
6992
|
+
}}</span>
|
|
6993
|
+
<span class="ngx-field__sr-only">{{ ui.text('ratingValue', language, { value: star, max: scale }) }}</span>
|
|
6994
|
+
</label>
|
|
6995
|
+
}
|
|
6996
|
+
@if (current !== null && !field.disabled) {
|
|
6997
|
+
<button
|
|
6998
|
+
type="button"
|
|
6999
|
+
class="ngx-field__remove-btn"
|
|
7000
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7001
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7002
|
+
(click)="clear()"
|
|
7003
|
+
>
|
|
7004
|
+
×
|
|
7005
|
+
</button>
|
|
7006
|
+
}
|
|
7007
|
+
</div>
|
|
7008
|
+
@if (errorMessage) {
|
|
7009
|
+
<span
|
|
7010
|
+
class="ngx-field__error"
|
|
7011
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7012
|
+
[id]="domId('-error')"
|
|
7013
|
+
role="alert"
|
|
7014
|
+
>{{ errorMessage }}</span
|
|
7015
|
+
>
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
</fieldset>
|
|
7019
|
+
</div>
|
|
7020
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7021
|
+
}
|
|
7022
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingFieldComponent, decorators: [{
|
|
7023
|
+
type: Component,
|
|
7024
|
+
args: [{
|
|
7025
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7026
|
+
selector: 'ngx-rating-field',
|
|
7027
|
+
standalone: true,
|
|
7028
|
+
imports: [ReactiveFormsModule],
|
|
7029
|
+
template: `
|
|
7030
|
+
<div
|
|
7031
|
+
class="ngx-field ngx-field--rating"
|
|
7032
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7033
|
+
[attr.data-field-type]="field.type"
|
|
7034
|
+
[class.ngx-field--readonly]="readonly"
|
|
7035
|
+
[class.ngx-field--masked]="masked"
|
|
7036
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7037
|
+
>
|
|
7038
|
+
<fieldset class="ngx-field__fieldset">
|
|
7039
|
+
<legend class="ngx-field__label">
|
|
7040
|
+
{{ label }}
|
|
7041
|
+
@if (field.validators?.required) {
|
|
7042
|
+
<span class="ngx-field__req">*</span>
|
|
7043
|
+
}
|
|
7044
|
+
@if (hint) {
|
|
7045
|
+
<!-- Inside the legend: a fieldset allows one legend, and it must come first. -->
|
|
7046
|
+
<span class="ngx-field__hint-wrap">
|
|
7047
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7048
|
+
<span
|
|
7049
|
+
class="ngx-field__hint"
|
|
7050
|
+
role="tooltip"
|
|
7051
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7052
|
+
[id]="domId('-hint')"
|
|
7053
|
+
>{{ hint }}</span
|
|
7054
|
+
>
|
|
7055
|
+
</span>
|
|
7056
|
+
}
|
|
7057
|
+
</legend>
|
|
7058
|
+
@if (masked) {
|
|
7059
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7060
|
+
maskedText
|
|
7061
|
+
}}</span>
|
|
7062
|
+
} @else if (readonly) {
|
|
7063
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7064
|
+
@if (current) {
|
|
7065
|
+
<span class="ngx-field__stars" aria-hidden="true">
|
|
7066
|
+
@for (star of stars; track star) {
|
|
7067
|
+
<span class="ngx-field__star-glyph" [class.ngx-field__star-glyph--on]="star <= current">{{
|
|
7068
|
+
star <= current ? '★' : '☆'
|
|
7069
|
+
}}</span>
|
|
7070
|
+
}
|
|
7071
|
+
</span>
|
|
7072
|
+
{{ ui.text('ratingValue', language, { value: current, max: scale }) }}
|
|
7073
|
+
} @else {
|
|
7074
|
+
—
|
|
7075
|
+
}
|
|
7076
|
+
</span>
|
|
7077
|
+
} @else {
|
|
7078
|
+
<div class="ngx-field__rating">
|
|
7079
|
+
@for (star of stars; track star) {
|
|
7080
|
+
<label class="ngx-field__star" [class.ngx-field__star--on]="current !== null && star <= current">
|
|
7081
|
+
<input
|
|
7082
|
+
type="radio"
|
|
7083
|
+
class="ngx-field__star-input"
|
|
7084
|
+
[id]="domId('-' + star)"
|
|
7085
|
+
[name]="domId()"
|
|
7086
|
+
[value]="star"
|
|
7087
|
+
[checked]="current === star"
|
|
7088
|
+
[attr.data-testid]="'field-' + field.id + '-star-' + star"
|
|
7089
|
+
[attr.aria-describedby]="describedBy()"
|
|
7090
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7091
|
+
(change)="select(star)"
|
|
7092
|
+
/>
|
|
7093
|
+
<!-- Filled and hollow, so the rating is readable without telling colours apart. -->
|
|
7094
|
+
<span class="ngx-field__star-glyph" aria-hidden="true">{{
|
|
7095
|
+
current !== null && star <= current ? '★' : '☆'
|
|
7096
|
+
}}</span>
|
|
7097
|
+
<span class="ngx-field__sr-only">{{ ui.text('ratingValue', language, { value: star, max: scale }) }}</span>
|
|
7098
|
+
</label>
|
|
7099
|
+
}
|
|
7100
|
+
@if (current !== null && !field.disabled) {
|
|
7101
|
+
<button
|
|
7102
|
+
type="button"
|
|
7103
|
+
class="ngx-field__remove-btn"
|
|
7104
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7105
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7106
|
+
(click)="clear()"
|
|
7107
|
+
>
|
|
7108
|
+
×
|
|
7109
|
+
</button>
|
|
7110
|
+
}
|
|
7111
|
+
</div>
|
|
7112
|
+
@if (errorMessage) {
|
|
7113
|
+
<span
|
|
7114
|
+
class="ngx-field__error"
|
|
7115
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7116
|
+
[id]="domId('-error')"
|
|
7117
|
+
role="alert"
|
|
7118
|
+
>{{ errorMessage }}</span
|
|
7119
|
+
>
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
</fieldset>
|
|
7123
|
+
</div>
|
|
7124
|
+
`,
|
|
7125
|
+
}]
|
|
7126
|
+
}], propDecorators: { field: [{
|
|
7127
|
+
type: Input
|
|
7128
|
+
}], language: [{
|
|
7129
|
+
type: Input
|
|
7130
|
+
}], readonly: [{
|
|
7131
|
+
type: Input
|
|
7132
|
+
}], masked: [{
|
|
7133
|
+
type: Input
|
|
7134
|
+
}], control: [{
|
|
7135
|
+
type: Input
|
|
7136
|
+
}] } });
|
|
7137
|
+
|
|
7138
|
+
/**
|
|
7139
|
+
* Colour field: a native colour picker, storing `#rrggbb`.
|
|
7140
|
+
*
|
|
7141
|
+
* Like a range input, a colour input has no empty state and shows black. The code beside it is
|
|
7142
|
+
* therefore the real value, a dash until a colour is chosen, and a clear button sets the value
|
|
7143
|
+
* back to nothing. Without them, "no colour" and "black" would look the same.
|
|
7144
|
+
*
|
|
7145
|
+
* Only a value that passes `normalizeHexColor` reaches a `style` binding. A record can hold
|
|
7146
|
+
* any string, and a colour swatch is no place to interpret one.
|
|
7147
|
+
*/
|
|
7148
|
+
class ColorFieldComponent {
|
|
7149
|
+
/**
|
|
7150
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
7151
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
7152
|
+
*/
|
|
7153
|
+
instanceId = nextFieldInstanceId();
|
|
7154
|
+
domId(suffix = '') {
|
|
7155
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
7156
|
+
}
|
|
7157
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
7158
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
7159
|
+
messages = inject(ValidationMessagesService);
|
|
7160
|
+
ui = inject(UiTextService);
|
|
7161
|
+
field;
|
|
7162
|
+
control;
|
|
7163
|
+
language = 'en';
|
|
7164
|
+
readonly = false;
|
|
7165
|
+
masked = false;
|
|
7166
|
+
get label() {
|
|
7167
|
+
return resolveLabel(this.field?.label, this.language);
|
|
7168
|
+
}
|
|
7169
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
7170
|
+
get hint() {
|
|
7171
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
7172
|
+
}
|
|
7173
|
+
/** The stored colour as `#rrggbb`, or null when there is none or it is not a colour. */
|
|
7174
|
+
get hex() {
|
|
7175
|
+
return normalizeHexColor(this.control?.value);
|
|
7176
|
+
}
|
|
7177
|
+
clear() {
|
|
7178
|
+
this.control?.setValue(null);
|
|
7179
|
+
this.control?.markAsTouched();
|
|
7180
|
+
this.control?.markAsDirty();
|
|
7181
|
+
}
|
|
7182
|
+
/** The ids of whatever is describing this control right now. */
|
|
7183
|
+
describedBy() {
|
|
7184
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
7185
|
+
}
|
|
7186
|
+
get errorMessage() {
|
|
7187
|
+
if (!this.control?.errors || !this.control.touched)
|
|
7188
|
+
return '';
|
|
7189
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
7190
|
+
}
|
|
7191
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ColorFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7192
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ColorFieldComponent, isStandalone: true, selector: "ngx-color-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
7193
|
+
<div
|
|
7194
|
+
class="ngx-field ngx-field--color"
|
|
7195
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7196
|
+
[attr.data-field-type]="field.type"
|
|
7197
|
+
[class.ngx-field--readonly]="readonly"
|
|
7198
|
+
[class.ngx-field--masked]="masked"
|
|
7199
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7200
|
+
>
|
|
7201
|
+
<div class="ngx-field__label-row">
|
|
7202
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7203
|
+
{{ label }}
|
|
7204
|
+
@if (field.validators?.required) {
|
|
7205
|
+
<span class="ngx-field__req">*</span>
|
|
7206
|
+
}
|
|
7207
|
+
</label>
|
|
7208
|
+
@if (hint) {
|
|
7209
|
+
<span class="ngx-field__hint-wrap">
|
|
7210
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7211
|
+
<span
|
|
7212
|
+
class="ngx-field__hint"
|
|
7213
|
+
role="tooltip"
|
|
7214
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7215
|
+
[id]="domId('-hint')"
|
|
7216
|
+
>{{ hint }}</span
|
|
7217
|
+
>
|
|
7218
|
+
</span>
|
|
7219
|
+
}
|
|
7220
|
+
</div>
|
|
7221
|
+
@if (masked) {
|
|
7222
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7223
|
+
maskedText
|
|
7224
|
+
}}</span>
|
|
7225
|
+
} @else if (readonly) {
|
|
7226
|
+
<span class="ngx-field__value ngx-field__color-value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7227
|
+
@if (hex) {
|
|
7228
|
+
<span class="ngx-field__swatch" [style.background-color]="hex" aria-hidden="true"></span>
|
|
7229
|
+
{{ hex }}
|
|
7230
|
+
} @else {
|
|
7231
|
+
{{ control.value || '—' }}
|
|
7232
|
+
}
|
|
7233
|
+
</span>
|
|
7234
|
+
} @else {
|
|
7235
|
+
<div class="ngx-field__color-wrap">
|
|
7236
|
+
<input
|
|
7237
|
+
[id]="domId()"
|
|
7238
|
+
type="color"
|
|
7239
|
+
class="ngx-field__color"
|
|
7240
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7241
|
+
[formControl]="$any(control)"
|
|
7242
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7243
|
+
[attr.aria-describedby]="describedBy()"
|
|
7244
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7245
|
+
/>
|
|
7246
|
+
<span class="ngx-field__color-code" [attr.data-testid]="'field-' + field.id + '-code'">{{ hex || '—' }}</span>
|
|
7247
|
+
@if (hex && !field.disabled) {
|
|
7248
|
+
<button
|
|
7249
|
+
type="button"
|
|
7250
|
+
class="ngx-field__remove-btn"
|
|
7251
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7252
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7253
|
+
(click)="clear()"
|
|
7254
|
+
>
|
|
7255
|
+
×
|
|
7256
|
+
</button>
|
|
7257
|
+
}
|
|
7258
|
+
</div>
|
|
7259
|
+
@if (errorMessage) {
|
|
7260
|
+
<span
|
|
7261
|
+
class="ngx-field__error"
|
|
7262
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7263
|
+
[id]="domId('-error')"
|
|
7264
|
+
role="alert"
|
|
7265
|
+
>{{ errorMessage }}</span
|
|
7266
|
+
>
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
</div>
|
|
7270
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7271
|
+
}
|
|
7272
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ColorFieldComponent, decorators: [{
|
|
7273
|
+
type: Component,
|
|
7274
|
+
args: [{
|
|
7275
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7276
|
+
selector: 'ngx-color-field',
|
|
7277
|
+
standalone: true,
|
|
7278
|
+
imports: [ReactiveFormsModule],
|
|
7279
|
+
template: `
|
|
7280
|
+
<div
|
|
7281
|
+
class="ngx-field ngx-field--color"
|
|
7282
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7283
|
+
[attr.data-field-type]="field.type"
|
|
7284
|
+
[class.ngx-field--readonly]="readonly"
|
|
7285
|
+
[class.ngx-field--masked]="masked"
|
|
7286
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7287
|
+
>
|
|
7288
|
+
<div class="ngx-field__label-row">
|
|
7289
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7290
|
+
{{ label }}
|
|
7291
|
+
@if (field.validators?.required) {
|
|
7292
|
+
<span class="ngx-field__req">*</span>
|
|
7293
|
+
}
|
|
7294
|
+
</label>
|
|
7295
|
+
@if (hint) {
|
|
7296
|
+
<span class="ngx-field__hint-wrap">
|
|
7297
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7298
|
+
<span
|
|
7299
|
+
class="ngx-field__hint"
|
|
7300
|
+
role="tooltip"
|
|
7301
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7302
|
+
[id]="domId('-hint')"
|
|
7303
|
+
>{{ hint }}</span
|
|
7304
|
+
>
|
|
7305
|
+
</span>
|
|
7306
|
+
}
|
|
7307
|
+
</div>
|
|
7308
|
+
@if (masked) {
|
|
7309
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7310
|
+
maskedText
|
|
7311
|
+
}}</span>
|
|
7312
|
+
} @else if (readonly) {
|
|
7313
|
+
<span class="ngx-field__value ngx-field__color-value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7314
|
+
@if (hex) {
|
|
7315
|
+
<span class="ngx-field__swatch" [style.background-color]="hex" aria-hidden="true"></span>
|
|
7316
|
+
{{ hex }}
|
|
7317
|
+
} @else {
|
|
7318
|
+
{{ control.value || '—' }}
|
|
7319
|
+
}
|
|
7320
|
+
</span>
|
|
7321
|
+
} @else {
|
|
7322
|
+
<div class="ngx-field__color-wrap">
|
|
7323
|
+
<input
|
|
7324
|
+
[id]="domId()"
|
|
7325
|
+
type="color"
|
|
7326
|
+
class="ngx-field__color"
|
|
7327
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7328
|
+
[formControl]="$any(control)"
|
|
7329
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7330
|
+
[attr.aria-describedby]="describedBy()"
|
|
7331
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7332
|
+
/>
|
|
7333
|
+
<span class="ngx-field__color-code" [attr.data-testid]="'field-' + field.id + '-code'">{{ hex || '—' }}</span>
|
|
7334
|
+
@if (hex && !field.disabled) {
|
|
7335
|
+
<button
|
|
7336
|
+
type="button"
|
|
7337
|
+
class="ngx-field__remove-btn"
|
|
7338
|
+
[attr.data-testid]="'field-' + field.id + '-clear'"
|
|
7339
|
+
[attr.aria-label]="ui.text('clearValue', language, { field: label })"
|
|
7340
|
+
(click)="clear()"
|
|
7341
|
+
>
|
|
7342
|
+
×
|
|
7343
|
+
</button>
|
|
7344
|
+
}
|
|
7345
|
+
</div>
|
|
7346
|
+
@if (errorMessage) {
|
|
7347
|
+
<span
|
|
7348
|
+
class="ngx-field__error"
|
|
7349
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7350
|
+
[id]="domId('-error')"
|
|
7351
|
+
role="alert"
|
|
7352
|
+
>{{ errorMessage }}</span
|
|
7353
|
+
>
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
</div>
|
|
7357
|
+
`,
|
|
7358
|
+
}]
|
|
7359
|
+
}], propDecorators: { field: [{
|
|
7360
|
+
type: Input
|
|
7361
|
+
}], control: [{
|
|
7362
|
+
type: Input
|
|
7363
|
+
}], language: [{
|
|
7364
|
+
type: Input
|
|
7365
|
+
}], readonly: [{
|
|
7366
|
+
type: Input
|
|
7367
|
+
}], masked: [{
|
|
7368
|
+
type: Input
|
|
7369
|
+
}] } });
|
|
7370
|
+
|
|
7371
|
+
/** What ends a tag while typing, or separates several in pasted text. */
|
|
7372
|
+
const TAG_SEPARATOR = /[,;\n]/;
|
|
7373
|
+
/**
|
|
7374
|
+
* Tags field: free text entered as a list of chips, stored as `string[]`.
|
|
7375
|
+
*
|
|
7376
|
+
* Unlike `multiSelect`, the values are whatever the user types, so there is no option list to
|
|
7377
|
+
* keep in step. Enter or a comma ends a tag, Backspace in an empty box removes the last one,
|
|
7378
|
+
* and pasted text is split on commas, semicolons and line breaks. Leaving the box commits
|
|
7379
|
+
* what is in it, so a tag someone typed and then tabbed away from is not silently lost.
|
|
7380
|
+
*
|
|
7381
|
+
* The control holds the array. The text box is only a draft, which is why it is not bound
|
|
7382
|
+
* with a form directive.
|
|
7383
|
+
*/
|
|
7384
|
+
class TagsFieldComponent {
|
|
7385
|
+
/**
|
|
7386
|
+
* Unique to this component instance: an `array` renders the same field once per row, and a
|
|
7387
|
+
* DOM id may not repeat. See `field-dom-id.ts`.
|
|
7388
|
+
*/
|
|
7389
|
+
instanceId = nextFieldInstanceId();
|
|
7390
|
+
domId(suffix = '') {
|
|
7391
|
+
return fieldDomId(this.field, this.instanceId, suffix);
|
|
7392
|
+
}
|
|
7393
|
+
/** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
|
|
7394
|
+
maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
|
|
7395
|
+
messages = inject(ValidationMessagesService);
|
|
7396
|
+
cdr = inject(ChangeDetectorRef);
|
|
7397
|
+
ui = inject(UiTextService);
|
|
7398
|
+
field;
|
|
7399
|
+
language = 'en';
|
|
7400
|
+
readonly = false;
|
|
7401
|
+
masked = false;
|
|
7402
|
+
_control;
|
|
7403
|
+
_controlSub;
|
|
7404
|
+
/** Subscribed for the same reason as the rating field: nothing here is a form directive. */
|
|
7405
|
+
set control(value) {
|
|
7406
|
+
this._control = value;
|
|
7407
|
+
this._controlSub?.unsubscribe();
|
|
7408
|
+
this._controlSub = this._control?.valueChanges?.subscribe(() => this.cdr.markForCheck());
|
|
7409
|
+
}
|
|
7410
|
+
get control() {
|
|
7411
|
+
return this._control;
|
|
7412
|
+
}
|
|
7413
|
+
ngOnDestroy() {
|
|
7414
|
+
this._controlSub?.unsubscribe();
|
|
7415
|
+
}
|
|
7416
|
+
get label() {
|
|
7417
|
+
return resolveLabel(this.field?.label, this.language);
|
|
7418
|
+
}
|
|
7419
|
+
get placeholder() {
|
|
7420
|
+
return resolveLabel(this.field?.placeholder, this.language);
|
|
7421
|
+
}
|
|
7422
|
+
/** Author help text, shown under the control and named by `aria-describedby`. */
|
|
7423
|
+
get hint() {
|
|
7424
|
+
return resolveLabel(this.field?.hint, this.language);
|
|
7425
|
+
}
|
|
7426
|
+
/** The stored tags, read leniently. A record may hold a single string or anything else. */
|
|
7427
|
+
get tags() {
|
|
7428
|
+
return normalizeTags(this.control?.value);
|
|
7429
|
+
}
|
|
7430
|
+
onKeydown(event, entry) {
|
|
7431
|
+
if (event.key === 'Enter' || event.key === ',' || event.key === ';') {
|
|
7432
|
+
// Enter inside a form submits it. Adding a tag is what the user meant.
|
|
7433
|
+
event.preventDefault();
|
|
7434
|
+
this.commit(entry);
|
|
7435
|
+
}
|
|
7436
|
+
else if (event.key === 'Backspace' && entry.value === '' && this.tags.length) {
|
|
7437
|
+
this.remove(this.tags[this.tags.length - 1]);
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
onPaste(event, entry) {
|
|
7441
|
+
const text = event.clipboardData?.getData('text') ?? '';
|
|
7442
|
+
if (!TAG_SEPARATOR.test(text))
|
|
7443
|
+
return;
|
|
7444
|
+
event.preventDefault();
|
|
7445
|
+
// Split here, not by writing the text into the box first: a single-line input strips line
|
|
7446
|
+
// breaks on assignment, which would join "z\nw" into one tag.
|
|
7447
|
+
const draft = entry.value;
|
|
7448
|
+
entry.value = '';
|
|
7449
|
+
this.add([draft, ...text.split(TAG_SEPARATOR)]);
|
|
7450
|
+
}
|
|
7451
|
+
onBlur(entry) {
|
|
7452
|
+
this.commit(entry);
|
|
7453
|
+
this.control?.markAsTouched();
|
|
7454
|
+
}
|
|
7455
|
+
/** Add whatever is in the draft box, split on separators, and empty it. */
|
|
7456
|
+
commit(entry) {
|
|
7457
|
+
const added = entry.value.split(TAG_SEPARATOR);
|
|
7458
|
+
entry.value = '';
|
|
7459
|
+
this.add(added);
|
|
7460
|
+
}
|
|
7461
|
+
add(candidates) {
|
|
7462
|
+
const next = normalizeTags([...this.tags, ...candidates]);
|
|
7463
|
+
if (next.length !== this.tags.length)
|
|
7464
|
+
this.write(next);
|
|
7465
|
+
}
|
|
7466
|
+
remove(tag) {
|
|
7467
|
+
this.write(this.tags.filter(t => t !== tag));
|
|
7468
|
+
}
|
|
7469
|
+
/**
|
|
7470
|
+
* An empty list is stored as `[]`, not `null`. `required` treats both as empty, and `[]`
|
|
7471
|
+
* stays the same shape as every other value this field holds.
|
|
7472
|
+
*/
|
|
7473
|
+
write(tags) {
|
|
7474
|
+
this.control?.setValue(tags);
|
|
7475
|
+
this.control?.markAsTouched();
|
|
7476
|
+
this.control?.markAsDirty();
|
|
7477
|
+
}
|
|
7478
|
+
/** The ids of whatever is describing this control right now. */
|
|
7479
|
+
describedBy() {
|
|
7480
|
+
return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
|
|
7481
|
+
}
|
|
7482
|
+
get errorMessage() {
|
|
7483
|
+
if (!this.control?.errors || !this.control.touched)
|
|
7484
|
+
return '';
|
|
7485
|
+
return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
|
|
7486
|
+
}
|
|
7487
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TagsFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7488
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TagsFieldComponent, isStandalone: true, selector: "ngx-tags-field", inputs: { field: "field", language: "language", readonly: "readonly", masked: "masked", control: "control" }, ngImport: i0, template: `
|
|
7489
|
+
<div
|
|
7490
|
+
class="ngx-field ngx-field--tags"
|
|
7491
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7492
|
+
[attr.data-field-type]="field.type"
|
|
7493
|
+
[class.ngx-field--readonly]="readonly"
|
|
7494
|
+
[class.ngx-field--masked]="masked"
|
|
7495
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7496
|
+
>
|
|
7497
|
+
<div class="ngx-field__label-row">
|
|
7498
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7499
|
+
{{ label }}
|
|
7500
|
+
@if (field.validators?.required) {
|
|
7501
|
+
<span class="ngx-field__req">*</span>
|
|
7502
|
+
}
|
|
7503
|
+
</label>
|
|
7504
|
+
@if (hint) {
|
|
7505
|
+
<span class="ngx-field__hint-wrap">
|
|
7506
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7507
|
+
<span
|
|
7508
|
+
class="ngx-field__hint"
|
|
7509
|
+
role="tooltip"
|
|
7510
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7511
|
+
[id]="domId('-hint')"
|
|
7512
|
+
>{{ hint }}</span
|
|
7513
|
+
>
|
|
7514
|
+
</span>
|
|
7515
|
+
}
|
|
7516
|
+
</div>
|
|
7517
|
+
@if (masked) {
|
|
7518
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7519
|
+
maskedText
|
|
7520
|
+
}}</span>
|
|
7521
|
+
} @else if (readonly) {
|
|
7522
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7523
|
+
@if (tags.length) {
|
|
7524
|
+
<span class="ngx-field__tags ngx-field__tags--readonly">
|
|
7525
|
+
@for (tag of tags; track tag) {
|
|
7526
|
+
<span class="ngx-field__tag">{{ tag }}</span>
|
|
7527
|
+
}
|
|
7528
|
+
</span>
|
|
7529
|
+
} @else {
|
|
7530
|
+
—
|
|
7531
|
+
}
|
|
7532
|
+
</span>
|
|
7533
|
+
} @else {
|
|
7534
|
+
<div class="ngx-field__tags" [class.ngx-field__tags--disabled]="field.disabled">
|
|
7535
|
+
@for (tag of tags; track tag) {
|
|
7536
|
+
<span class="ngx-field__tag" [attr.data-testid]="'field-' + field.id + '-tag'">
|
|
7537
|
+
{{ tag }}
|
|
7538
|
+
@if (!field.disabled) {
|
|
7539
|
+
<button
|
|
7540
|
+
type="button"
|
|
7541
|
+
class="ngx-field__tag-remove"
|
|
7542
|
+
[attr.aria-label]="ui.text('removeTag', language, { tag: tag })"
|
|
7543
|
+
(click)="remove(tag)"
|
|
7544
|
+
>
|
|
7545
|
+
×
|
|
7546
|
+
</button>
|
|
7547
|
+
}
|
|
7548
|
+
</span>
|
|
7549
|
+
}
|
|
7550
|
+
<input
|
|
7551
|
+
#entry
|
|
7552
|
+
[id]="domId()"
|
|
7553
|
+
type="text"
|
|
7554
|
+
class="ngx-field__tags-input"
|
|
7555
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7556
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7557
|
+
[attr.aria-describedby]="describedBy()"
|
|
7558
|
+
[placeholder]="placeholder || ui.text('addTagPlaceholder', language)"
|
|
7559
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7560
|
+
(keydown)="onKeydown($event, entry)"
|
|
7561
|
+
(paste)="onPaste($event, entry)"
|
|
7562
|
+
(blur)="onBlur(entry)"
|
|
7563
|
+
/>
|
|
7564
|
+
</div>
|
|
7565
|
+
@if (errorMessage) {
|
|
7566
|
+
<span
|
|
7567
|
+
class="ngx-field__error"
|
|
7568
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7569
|
+
[id]="domId('-error')"
|
|
7570
|
+
role="alert"
|
|
7571
|
+
>{{ errorMessage }}</span
|
|
7572
|
+
>
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7575
|
+
</div>
|
|
7576
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7577
|
+
}
|
|
7578
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TagsFieldComponent, decorators: [{
|
|
7579
|
+
type: Component,
|
|
7580
|
+
args: [{
|
|
7581
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7582
|
+
selector: 'ngx-tags-field',
|
|
7583
|
+
standalone: true,
|
|
7584
|
+
template: `
|
|
7585
|
+
<div
|
|
7586
|
+
class="ngx-field ngx-field--tags"
|
|
7587
|
+
[attr.data-testid]="'field-' + field.id"
|
|
7588
|
+
[attr.data-field-type]="field.type"
|
|
7589
|
+
[class.ngx-field--readonly]="readonly"
|
|
7590
|
+
[class.ngx-field--masked]="masked"
|
|
7591
|
+
[class.ngx-field--invalid]="control && control.invalid && control.touched"
|
|
7592
|
+
>
|
|
7593
|
+
<div class="ngx-field__label-row">
|
|
7594
|
+
<label class="ngx-field__label" [attr.for]="domId()">
|
|
7595
|
+
{{ label }}
|
|
7596
|
+
@if (field.validators?.required) {
|
|
7597
|
+
<span class="ngx-field__req">*</span>
|
|
7598
|
+
}
|
|
7599
|
+
</label>
|
|
7600
|
+
@if (hint) {
|
|
7601
|
+
<span class="ngx-field__hint-wrap">
|
|
7602
|
+
<span class="ngx-field__hint-icon" aria-hidden="true">i</span>
|
|
7603
|
+
<span
|
|
7604
|
+
class="ngx-field__hint"
|
|
7605
|
+
role="tooltip"
|
|
7606
|
+
[attr.data-testid]="'field-' + field.id + '-hint'"
|
|
7607
|
+
[id]="domId('-hint')"
|
|
7608
|
+
>{{ hint }}</span
|
|
7609
|
+
>
|
|
7610
|
+
</span>
|
|
7611
|
+
}
|
|
7612
|
+
</div>
|
|
7613
|
+
@if (masked) {
|
|
7614
|
+
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
|
|
7615
|
+
maskedText
|
|
7616
|
+
}}</span>
|
|
7617
|
+
} @else if (readonly) {
|
|
7618
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
|
|
7619
|
+
@if (tags.length) {
|
|
7620
|
+
<span class="ngx-field__tags ngx-field__tags--readonly">
|
|
7621
|
+
@for (tag of tags; track tag) {
|
|
7622
|
+
<span class="ngx-field__tag">{{ tag }}</span>
|
|
7623
|
+
}
|
|
7624
|
+
</span>
|
|
7625
|
+
} @else {
|
|
7626
|
+
—
|
|
7627
|
+
}
|
|
7628
|
+
</span>
|
|
7629
|
+
} @else {
|
|
7630
|
+
<div class="ngx-field__tags" [class.ngx-field__tags--disabled]="field.disabled">
|
|
7631
|
+
@for (tag of tags; track tag) {
|
|
7632
|
+
<span class="ngx-field__tag" [attr.data-testid]="'field-' + field.id + '-tag'">
|
|
7633
|
+
{{ tag }}
|
|
7634
|
+
@if (!field.disabled) {
|
|
7635
|
+
<button
|
|
7636
|
+
type="button"
|
|
7637
|
+
class="ngx-field__tag-remove"
|
|
7638
|
+
[attr.aria-label]="ui.text('removeTag', language, { tag: tag })"
|
|
7639
|
+
(click)="remove(tag)"
|
|
7640
|
+
>
|
|
7641
|
+
×
|
|
7642
|
+
</button>
|
|
7643
|
+
}
|
|
7644
|
+
</span>
|
|
7645
|
+
}
|
|
7646
|
+
<input
|
|
7647
|
+
#entry
|
|
7648
|
+
[id]="domId()"
|
|
7649
|
+
type="text"
|
|
7650
|
+
class="ngx-field__tags-input"
|
|
7651
|
+
[attr.data-testid]="'field-' + field.id + '-input'"
|
|
7652
|
+
[attr.aria-invalid]="control.invalid && control.touched"
|
|
7653
|
+
[attr.aria-describedby]="describedBy()"
|
|
7654
|
+
[placeholder]="placeholder || ui.text('addTagPlaceholder', language)"
|
|
7655
|
+
[attr.disabled]="field.disabled ? true : null"
|
|
7656
|
+
(keydown)="onKeydown($event, entry)"
|
|
7657
|
+
(paste)="onPaste($event, entry)"
|
|
7658
|
+
(blur)="onBlur(entry)"
|
|
7659
|
+
/>
|
|
7660
|
+
</div>
|
|
7661
|
+
@if (errorMessage) {
|
|
7662
|
+
<span
|
|
7663
|
+
class="ngx-field__error"
|
|
7664
|
+
[attr.data-testid]="'field-' + field.id + '-error'"
|
|
7665
|
+
[id]="domId('-error')"
|
|
7666
|
+
role="alert"
|
|
7667
|
+
>{{ errorMessage }}</span
|
|
7668
|
+
>
|
|
7669
|
+
}
|
|
7670
|
+
}
|
|
7671
|
+
</div>
|
|
7672
|
+
`,
|
|
7673
|
+
}]
|
|
7674
|
+
}], propDecorators: { field: [{
|
|
7675
|
+
type: Input
|
|
7676
|
+
}], language: [{
|
|
7677
|
+
type: Input
|
|
7678
|
+
}], readonly: [{
|
|
7679
|
+
type: Input
|
|
7680
|
+
}], masked: [{
|
|
7681
|
+
type: Input
|
|
7682
|
+
}], control: [{
|
|
7683
|
+
type: Input
|
|
7684
|
+
}] } });
|
|
7685
|
+
|
|
6154
7686
|
/**
|
|
6155
7687
|
* Field-type registration — the tree-shaking seam.
|
|
6156
7688
|
*
|
|
6157
7689
|
* `FieldRegistryService` imports no components. Whatever you register here is what gets
|
|
6158
7690
|
* bundled, so an app that only uses text/number/dropdown pays for three components and
|
|
6159
|
-
* their imports, not twenty-
|
|
7691
|
+
* their imports, not twenty-seven.
|
|
6160
7692
|
*
|
|
6161
7693
|
* ```ts
|
|
6162
7694
|
* // Everything (convenient; bundles all built-ins):
|
|
@@ -6192,6 +7724,12 @@ function builtInFieldTypes() {
|
|
|
6192
7724
|
array: ArrayFieldComponent,
|
|
6193
7725
|
image: ImageFieldComponent,
|
|
6194
7726
|
file: FileFieldComponent,
|
|
7727
|
+
url: UrlFieldComponent,
|
|
7728
|
+
phone: PhoneFieldComponent,
|
|
7729
|
+
slider: SliderFieldComponent,
|
|
7730
|
+
rating: RatingFieldComponent,
|
|
7731
|
+
color: ColorFieldComponent,
|
|
7732
|
+
tags: TagsFieldComponent,
|
|
6195
7733
|
};
|
|
6196
7734
|
}
|
|
6197
7735
|
/** Register a specific set of field types. Only the components you name are bundled. */
|
|
@@ -6200,7 +7738,7 @@ function provideFieldTypes(types) {
|
|
|
6200
7738
|
{ provide: FIELD_TYPE_SETS, useValue: types, multi: true },
|
|
6201
7739
|
]);
|
|
6202
7740
|
}
|
|
6203
|
-
/** Register all
|
|
7741
|
+
/** Register all 27 built-in field type keys. Convenience for apps that use most of them. */
|
|
6204
7742
|
function provideBuiltInFieldTypes() {
|
|
6205
7743
|
return provideFieldTypes(builtInFieldTypes());
|
|
6206
7744
|
}
|
|
@@ -6716,6 +8254,12 @@ const AUTO_COL_SPAN = {
|
|
|
6716
8254
|
boolean: 4,
|
|
6717
8255
|
checkbox: 4,
|
|
6718
8256
|
'entity-ref': 6,
|
|
8257
|
+
url: 6,
|
|
8258
|
+
phone: 6,
|
|
8259
|
+
slider: 6,
|
|
8260
|
+
rating: 4,
|
|
8261
|
+
color: 4,
|
|
8262
|
+
tags: 6,
|
|
6719
8263
|
// Left at the full width deliberately — textarea, markdown, image, file, group and array
|
|
6720
8264
|
// each hold something a half-row would crop.
|
|
6721
8265
|
};
|
|
@@ -9972,5 +11516,5 @@ function saveBlob(blob, filename) {
|
|
|
9972
11516
|
* Generated bundle index. Do not edit.
|
|
9973
11517
|
*/
|
|
9974
11518
|
|
|
9975
|
-
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityImportComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, HttpImportTransport, IMPORT_TRANSPORT, ImageFieldComponent, ImportErrorsComponent, ImportMapperComponent, ImportPreviewComponent, ImportTemplateComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LocalImportTransport, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText };
|
|
11519
|
+
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ColorFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityImportComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, HttpImportTransport, IMPORT_TRANSPORT, ImageFieldComponent, ImportErrorsComponent, ImportMapperComponent, ImportPreviewComponent, ImportTemplateComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LocalImportTransport, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, PhoneFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RatingFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, SliderFieldComponent, TagsFieldComponent, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, UrlFieldComponent, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, phoneValidator, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText, urlValidator };
|
|
9976
11520
|
//# sourceMappingURL=ngx-dynamic-entity.mjs.map
|