UNPKG

@ng-formworks/core

Version:

Angular ng-formworks - JSON Schema Form builder core

1,216 lines (1,206 loc) 619 kB
import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, inject, NgZone, input, viewChild, ViewContainerRef, Component, Input, Directive, ChangeDetectionStrategy, ViewChild, ChangeDetectorRef, signal, Pipe, Inject, ElementRef, NgModule, forwardRef, output } from '@angular/core'; import * as i1 from '@angular/forms'; import { UntypedFormControl, UntypedFormArray, UntypedFormGroup, FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import addFormats from 'ajv-formats'; import Ajv2019 from 'ajv/dist/2019'; import jsonDraft6 from 'ajv/lib/refs/json-schema-draft-06.json'; import jsonDraft7 from 'ajv/lib/refs/json-schema-draft-07.json'; import cloneDeep from 'lodash/cloneDeep'; import _isArray from 'lodash/isArray'; import _template from 'lodash/template'; import { from, Observable, forkJoin, Subject, BehaviorSubject, debounceTime, distinctUntilChanged, of, lastValueFrom } from 'rxjs'; import { some, isNil, isEmpty as isEmpty$1, pick, isObject as isObject$1, isEqual as isEqual$2, memoize } from 'lodash'; import isEqual$1 from 'lodash/isEqual'; import { map, takeUntil } from 'rxjs/operators'; import omit from 'lodash/omit'; import filter from 'lodash/filter'; import map$1 from 'lodash/map'; import _isPlainObject from 'lodash/isPlainObject'; import uniqueId from 'lodash/uniqueId'; import { Eta } from 'eta/core'; import * as i1$1 from '@angular/cdk/drag-drop'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { HttpClient } from '@angular/common/http'; class Framework { constructor() { this.widgets = {}; this.stylesheets = []; this.scripts = []; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: Framework, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: Framework }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: Framework, decorators: [{ type: Injectable }] }); const deValidationMessages = { required: 'Darf nicht leer sein', minLength: 'Mindestens {{minimumLength}} Zeichen benötigt (aktuell: {{currentLength}})', maxLength: 'Maximal {{maximumLength}} Zeichen erlaubt (aktuell: {{currentLength}})', pattern: 'Entspricht nicht diesem regulären Ausdruck: {{requiredPattern}}', format: function (error) { switch (error.requiredFormat) { case 'date': return 'Muss ein Datum sein, z. B. "2000-12-31"'; case 'time': return 'Muss eine Zeitangabe sein, z. B. "16:20" oder "03:14:15.9265"'; case 'date-time': return 'Muss Datum mit Zeit beinhalten, z. B. "2000-03-14T01:59" oder "2000-03-14T01:59:26.535Z"'; case 'email': return 'Keine gültige E-Mail-Adresse (z. B. "name@example.com")'; case 'hostname': return 'Kein gültiger Hostname (z. B. "example.com")'; case 'ipv4': return 'Keine gültige IPv4-Adresse (z. B. "127.0.0.1")'; case 'ipv6': return 'Keine gültige IPv6-Adresse (z. B. "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0")'; // TODO: add examples for 'uri', 'uri-reference', and 'uri-template' // case 'uri': case 'uri-reference': case 'uri-template': case 'url': return 'Keine gültige URL (z. B. "http://www.example.com/page.html")'; case 'uuid': return 'Keine gültige UUID (z. B. "12345678-9ABC-DEF0-1234-56789ABCDEF0")'; case 'color': return 'Kein gültiger Farbwert (z. B. "#FFFFFF")'; case 'json-pointer': return 'Kein gültiger JSON-Pointer (z. B. "/pointer/to/something")'; case 'relative-json-pointer': return 'Kein gültiger relativer JSON-Pointer (z. B. "2/pointer/to/something")'; case 'regex': return 'Kein gültiger regulärer Ausdruck (z. B. "(1-)?\\d{3}-\\d{3}-\\d{4}")'; case 'duration': return "Muss eine gültige ISO 8601-Dauer sein (z. B. 'PT1H30M')"; default: return 'Muss diesem Format entsprechen: ' + error.requiredFormat; } }, minimum: 'Muss mindestens {{minimumValue}} sein', exclusiveMinimum: 'Muss größer als {{exclusiveMinimumValue}} sein', maximum: 'Darf maximal {{maximumValue}} sein', exclusiveMaximum: 'Muss kleiner als {{exclusiveMaximumValue}} sein', multipleOf: function (error) { if ((1 / error.multipleOfValue) % 10 === 0) { const decimals = Math.log10(1 / error.multipleOfValue); return `Maximal ${decimals} Dezimalstellen erlaubt`; } else { return `Muss ein Vielfaches von ${error.multipleOfValue} sein`; } }, minProperties: 'Mindestens {{minimumProperties}} Attribute erforderlich (aktuell: {{currentProperties}})', maxProperties: 'Maximal {{maximumProperties}} Attribute erlaubt (aktuell: {{currentProperties}})', minItems: 'Mindestens {{minimumItems}} Werte erforderlich (aktuell: {{currentItems}})', maxItems: 'Maximal {{maximumItems}} Werte erlaubt (aktuell: {{currentItems}})', uniqueItems: 'Alle Werte müssen eindeutig sein', // Note: No default error messages for 'type', 'const', 'enum', or 'dependencies' }; const enValidationMessages = { required: 'This field is required.', minLength: 'Must be {{minimumLength}} characters or longer (current length: {{currentLength}})', maxLength: 'Must be {{maximumLength}} characters or shorter (current length: {{currentLength}})', pattern: 'Must match pattern: {{requiredPattern}}', format: function (error) { switch (error.requiredFormat) { case 'date': return 'Must be a date, like "2000-12-31"'; case 'time': return 'Must be a time, like "16:20" or "03:14:15.9265"'; case 'date-time': return 'Must be a date-time, like "2000-03-14T01:59" or "2000-03-14T01:59:26.535Z"'; case 'email': return 'Must be an email address, like "name@example.com"'; case 'hostname': return 'Must be a hostname, like "example.com"'; case 'ipv4': return 'Must be an IPv4 address, like "127.0.0.1"'; case 'ipv6': return 'Must be an IPv6 address, like "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0"'; // TODO: add examples for 'uri', 'uri-reference', and 'uri-template' // case 'uri': case 'uri-reference': case 'uri-template': case 'url': return 'Must be a url, like "http://www.example.com/page.html"'; case 'uuid': return 'Must be a uuid, like "12345678-9ABC-DEF0-1234-56789ABCDEF0"'; case 'color': return 'Must be a color, like "#FFFFFF"'; case 'json-pointer': return 'Must be a JSON Pointer, like "/pointer/to/something"'; case 'relative-json-pointer': return 'Must be a relative JSON Pointer, like "2/pointer/to/something"'; case 'regex': return 'Must be a regular expression, like "(1-)?\\d{3}-\\d{3}-\\d{4}"'; case 'duration': return "Must be a valid ISO 8601 duration (e.g., 'PT1H30M')"; default: return 'Must be a correctly formatted ' + error.requiredFormat; } }, minimum: 'Must be {{minimumValue}} or more', exclusiveMinimum: 'Must be more than {{exclusiveMinimumValue}}', maximum: 'Must be {{maximumValue}} or less', exclusiveMaximum: 'Must be less than {{exclusiveMaximumValue}}', multipleOf: function (error) { if ((1 / error.multipleOfValue) % 10 === 0) { const decimals = Math.log10(1 / error.multipleOfValue); return `Must have ${decimals} or fewer decimal places.`; } else { return `Must be a multiple of ${error.multipleOfValue}.`; } }, minProperties: 'Must have {{minimumProperties}} or more items (current items: {{currentProperties}})', maxProperties: 'Must have {{maximumProperties}} or fewer items (current items: {{currentProperties}})', minItems: 'Must have {{minimumItems}} or more items (current items: {{currentItems}})', maxItems: 'Must have {{maximumItems}} or fewer items (current items: {{currentItems}})', uniqueItems: 'All items must be unique', // Note: No default error messages for 'type', 'const', 'enum', or 'dependencies' }; const esValidationMessages = { required: 'Este campo está requerido.', minLength: 'Debe tener {{minimumLength}} caracteres o más longitud (longitud actual: {{currentLength}})', maxLength: 'Debe tener {{maximumLength}} caracteres o menos longitud (longitud actual: {{currentLength}})', pattern: 'Must match pattern: {{requiredPattern}}', format: function (error) { switch (error.requiredFormat) { case 'date': return 'Debe tener una fecha, ej "2000-12-31"'; case 'time': return 'Debe tener una hora, ej "16:20" o "03:14:15.9265"'; case 'date-time': return 'Debe tener fecha y hora, ej "2000-03-14T01:59" o "2000-03-14T01:59:26.535Z"'; case 'email': return 'No hay dirección de correo electrónico válida, ej "name@example.com"'; case 'hostname': return 'Debe ser un nombre de host válido, ej "example.com"'; case 'ipv4': return 'Debe ser una dirección de IPv4, ej "127.0.0.1"'; case 'ipv6': return 'Debe ser una dirección de IPv6, ej "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0"'; case 'url': return 'Debe ser una URL, ej "http://www.example.com/page.html"'; case 'uuid': return 'Debe ser un UUID, ej "12345678-9ABC-DEF0-1234-56789ABCDEF0"'; case 'color': return 'Debe ser un color, ej "#FFFFFF"'; case 'json-pointer': return 'Debe ser un JSON Pointer, ej "/pointer/to/something"'; case 'relative-json-pointer': return 'Debe ser un JSON Pointer relativo, ej "2/pointer/to/something"'; case 'regex': return 'Debe ser una expresión regular, ej "(1-)?\\d{3}-\\d{3}-\\d{4}"'; case 'duration': return "Debe ser una duración válida en formato ISO 8601 (p. ej., 'PT1H30M')"; default: return 'Debe tener el formato correcto ' + error.requiredFormat; } }, minimum: 'Debe ser {{minimumValue}} o más', exclusiveMinimum: 'Debe ser superior a {{exclusiveMinimumValue}}', maximum: 'Debe ser {{maximumValue}} o menos', exclusiveMaximum: 'Debe ser menor que {{exclusiveMaximumValue}}', multipleOf: function (error) { if ((1 / error.multipleOfValue) % 10 === 0) { const decimals = Math.log10(1 / error.multipleOfValue); return `Se permite un máximo de ${decimals} decimales`; } else { return `Debe ser múltiplo de ${error.multipleOfValue}.`; } }, minProperties: 'Debe tener {{minimumProperties}} o más elementos (elementos actuales: {{currentProperties}})', maxProperties: 'Debe tener {{maximumProperties}} o menos elementos (elementos actuales: {{currentProperties}})', minItems: 'Debe tener {{minimumItems}} o más elementos (elementos actuales: {{currentItems}})', maxItems: 'Debe tener {{maximumItems}} o menos elementos (elementos actuales: {{currentItems}})', uniqueItems: 'Todos los elementos deben ser únicos', }; const frValidationMessages = { required: 'Est obligatoire.', minLength: 'Doit avoir minimum {{minimumLength}} caractères (actuellement: {{currentLength}})', maxLength: 'Doit avoir maximum {{maximumLength}} caractères (actuellement: {{currentLength}})', pattern: 'Doit respecter: {{requiredPattern}}', format: function (error) { switch (error.requiredFormat) { case 'date': return 'Doit être une date, tel que "2000-12-31"'; case 'time': return 'Doit être une heure, tel que "16:20" ou "03:14:15.9265"'; case 'date-time': return 'Doit être une date et une heure, tel que "2000-03-14T01:59" ou "2000-03-14T01:59:26.535Z"'; case 'email': return 'Doit être une adresse e-mail, tel que "name@example.com"'; case 'hostname': return 'Doit être un nom de domaine, tel que "example.com"'; case 'ipv4': return 'Doit être une adresse IPv4, tel que "127.0.0.1"'; case 'ipv6': return 'Doit être une adresse IPv6, tel que "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0"'; // TODO: add examples for 'uri', 'uri-reference', and 'uri-template' // case 'uri': case 'uri-reference': case 'uri-template': case 'url': return 'Doit être une URL, tel que "http://www.example.com/page.html"'; case 'uuid': return 'Doit être un UUID, tel que "12345678-9ABC-DEF0-1234-56789ABCDEF0"'; case 'color': return 'Doit être une couleur, tel que "#FFFFFF"'; case 'json-pointer': return 'Doit être un JSON Pointer, tel que "/pointer/to/something"'; case 'relative-json-pointer': return 'Doit être un relative JSON Pointer, tel que "2/pointer/to/something"'; case 'regex': return 'Doit être une expression régulière, tel que "(1-)?\\d{3}-\\d{3}-\\d{4}"'; case 'duration': return "Doit être une durée valide au format ISO 8601 (par ex., 'PT1H30M')"; default: return 'Doit être avoir le format correct: ' + error.requiredFormat; } }, minimum: 'Doit être supérieur à {{minimumValue}}', exclusiveMinimum: 'Doit avoir minimum {{exclusiveMinimumValue}} charactères', maximum: 'Doit être inférieur à {{maximumValue}}', exclusiveMaximum: 'Doit avoir maximum {{exclusiveMaximumValue}} charactères', multipleOf: function (error) { if ((1 / error.multipleOfValue) % 10 === 0) { const decimals = Math.log10(1 / error.multipleOfValue); return `Doit comporter ${decimals} ou moins de decimales.`; } else { return `Doit être un multiple de ${error.multipleOfValue}.`; } }, minProperties: 'Doit comporter au minimum {{minimumProperties}} éléments', maxProperties: 'Doit comporter au maximum {{maximumProperties}} éléments', minItems: 'Doit comporter au minimum {{minimumItems}} éléments', maxItems: 'Doit comporter au maximum {{minimumItems}} éléments', uniqueItems: 'Tous les éléments doivent être uniques', // Note: No default error messages for 'type', 'const', 'enum', or 'dependencies' }; const itValidationMessages = { required: 'Il campo è obbligatorio', minLength: 'Deve inserire almeno {{minimumLength}} caratteri (lunghezza corrente: {{currentLength}})', maxLength: 'Il numero massimo di caratteri consentito è {{maximumLength}} (lunghezza corrente: {{currentLength}})', pattern: 'Devi rispettare il pattern : {{requiredPattern}}', format: function (error) { switch (error.requiredFormat) { case 'date': return 'Deve essere una data, come "31-12-2000"'; case 'time': return 'Deve essere un orario, come "16:20" o "03:14:15.9265"'; case 'date-time': return 'Deve essere data-orario, come "14-03-2000T01:59" or "14-03-2000T01:59:26.535Z"'; case 'email': return 'Deve essere un indirzzo email, come "name@example.com"'; case 'hostname': return 'Deve essere un hostname, come "example.com"'; case 'ipv4': return 'Deve essere un indirizzo IPv4, come "127.0.0.1"'; case 'ipv6': return 'Deve essere un indirizzo IPv6, come "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0"'; // TODO: add examples for 'uri', 'uri-reference', and 'uri-template' // case 'uri': case 'uri-reference': case 'uri-template': case 'url': return 'Deve essere un url, come "http://www.example.com/page.html"'; case 'uuid': return 'Deve essere un uuid, come "12345678-9ABC-DEF0-1234-56789ABCDEF0"'; case 'color': return 'Deve essere un colore, come "#FFFFFF"'; case 'json-pointer': return 'Deve essere un JSON Pointer, come "/pointer/to/something"'; case 'relative-json-pointer': return 'Deve essere un JSON Pointer relativo, come "2/pointer/to/something"'; case 'regex': return 'Deve essere una regular expression, come "(1-)?\\d{3}-\\d{3}-\\d{4}"'; case 'duration': return "Deve essere una durata valida nel formato ISO 8601 (es. 'PT1H30M')"; default: return 'Deve essere formattato correttamente ' + error.requiredFormat; } }, minimum: 'Deve essere {{minimumValue}} o più', exclusiveMinimum: 'Deve essere più di {{exclusiveMinimumValue}}', maximum: 'Deve essere {{maximumValue}} o meno', exclusiveMaximum: 'Deve essere minore di {{exclusiveMaximumValue}}', multipleOf: function (error) { if ((1 / error.multipleOfValue) % 10 === 0) { const decimals = Math.log10(1 / error.multipleOfValue); return `Deve avere ${decimals} o meno decimali.`; } else { return `Deve essere multiplo di ${error.multipleOfValue}.`; } }, minProperties: 'Deve avere {{minimumProperties}} o più elementi (elementi correnti: {{currentProperties}})', maxProperties: 'Deve avere {{maximumProperties}} o meno elementi (elementi correnti: {{currentProperties}})', minItems: 'Deve avere {{minimumItems}} o più elementi (elementi correnti: {{currentItems}})', maxItems: 'Deve avere {{maximumItems}} o meno elementi (elementi correnti: {{currentItems}})', uniqueItems: 'Tutti gli elementi devono essere unici', // Note: No default error messages for 'type', 'const', 'enum', or 'dependencies' }; const ptValidationMessages = { required: 'Este campo é obrigatório.', minLength: 'É preciso no mínimo {{minimumLength}} caracteres ou mais (tamanho atual: {{currentLength}})', maxLength: 'É preciso no máximo {{maximumLength}} caracteres ou menos (tamanho atual: {{currentLength}})', pattern: 'Tem que ajustar ao formato: {{requiredPattern}}', format: function (error) { switch (error.requiredFormat) { case 'date': return 'Tem que ser uma data, por exemplo "2000-12-31"'; case 'time': return 'Tem que ser horário, por exemplo "16:20" ou "03:14:15.9265"'; case 'date-time': return 'Tem que ser data e hora, por exemplo "2000-03-14T01:59" ou "2000-03-14T01:59:26.535Z"'; case 'email': return 'Tem que ser um email, por exemplo "fulano@exemplo.com.br"'; case 'hostname': return 'Tem que ser uma nome de domínio, por exemplo "exemplo.com.br"'; case 'ipv4': return 'Tem que ser um endereço IPv4, por exemplo "127.0.0.1"'; case 'ipv6': return 'Tem que ser um endereço IPv6, por exemplo "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0"'; // TODO: add examples for 'uri', 'uri-reference', and 'uri-template' // case 'uri': case 'uri-reference': case 'uri-template': case 'url': return 'Tem que ser uma URL, por exemplo "http://www.exemplo.com.br/pagina.html"'; case 'uuid': return 'Tem que ser um uuid, por exemplo "12345678-9ABC-DEF0-1234-56789ABCDEF0"'; case 'color': return 'Tem que ser uma cor, por exemplo "#FFFFFF"'; case 'json-pointer': return 'Tem que ser um JSON Pointer, por exemplo "/referencia/para/algo"'; case 'relative-json-pointer': return 'Tem que ser um JSON Pointer relativo, por exemplo "2/referencia/para/algo"'; case 'regex': return 'Tem que ser uma expressão regular, por exemplo "(1-)?\\d{3}-\\d{3}-\\d{4}"'; case 'duration': return "Deve ser uma duração válida no formato ISO 8601 (ex.: 'PT1H30M')"; default: return 'Tem que ser no formato: ' + error.requiredFormat; } }, minimum: 'Tem que ser {{minimumValue}} ou mais', exclusiveMinimum: 'Tem que ser mais que {{exclusiveMinimumValue}}', maximum: 'Tem que ser {{maximumValue}} ou menos', exclusiveMaximum: 'Tem que ser menor que {{exclusiveMaximumValue}}', multipleOf: function (error) { if ((1 / error.multipleOfValue) % 10 === 0) { const decimals = Math.log10(1 / error.multipleOfValue); return `Tem que ter ${decimals} ou menos casas decimais.`; } else { return `Tem que ser um múltiplo de ${error.multipleOfValue}.`; } }, minProperties: 'Deve ter {{minimumProperties}} ou mais itens (itens até o momento: {{currentProperties}})', maxProperties: 'Deve ter {{maximumProperties}} ou menos intens (itens até o momento: {{currentProperties}})', minItems: 'Deve ter {{minimumItems}} ou mais itens (itens até o momento: {{currentItems}})', maxItems: 'Deve ter {{maximumItems}} ou menos itens (itens até o momento: {{currentItems}})', uniqueItems: 'Todos os itens devem ser únicos', // Note: No default error messages for 'type', 'const', 'enum', or 'dependencies' }; const zhValidationMessages = { required: '必填字段.', minLength: '字符长度必须大于或者等于 {{minimumLength}} (当前长度: {{currentLength}})', maxLength: '字符长度必须小于或者等于 {{maximumLength}} (当前长度: {{currentLength}})', pattern: '必须匹配正则表达式: {{requiredPattern}}', format: function (error) { switch (error.requiredFormat) { case 'date': return '必须为日期格式, 比如 "2000-12-31"'; case 'time': return '必须为时间格式, 比如 "16:20" 或者 "03:14:15.9265"'; case 'date-time': return '必须为日期时间格式, 比如 "2000-03-14T01:59" 或者 "2000-03-14T01:59:26.535Z"'; case 'email': return '必须为邮箱地址, 比如 "name@example.com"'; case 'hostname': return '必须为主机名, 比如 "example.com"'; case 'ipv4': return '必须为 IPv4 地址, 比如 "127.0.0.1"'; case 'ipv6': return '必须为 IPv6 地址, 比如 "1234:5678:9ABC:DEF0:1234:5678:9ABC:DEF0"'; // TODO: add examples for 'uri', 'uri-reference', and 'uri-template' // case 'uri': case 'uri-reference': case 'uri-template': case 'url': return '必须为 url, 比如 "http://www.example.com/page.html"'; case 'uuid': return '必须为 uuid, 比如 "12345678-9ABC-DEF0-1234-56789ABCDEF0"'; case 'color': return '必须为颜色值, 比如 "#FFFFFF"'; case 'json-pointer': return '必须为 JSON Pointer, 比如 "/pointer/to/something"'; case 'relative-json-pointer': return '必须为相对的 JSON Pointer, 比如 "2/pointer/to/something"'; case 'regex': return '必须为正则表达式, 比如 "(1-)?\\d{3}-\\d{3}-\\d{4}"'; case 'duration': return "必须是有效的 ISO 8601 持续时间(例如:'PT1H30M')"; default: return '必须为格式正确的 ' + error.requiredFormat; } }, minimum: '必须大于或者等于最小值: {{minimumValue}}', exclusiveMinimum: '必须大于最小值: {{exclusiveMinimumValue}}', maximum: '必须小于或者等于最大值: {{maximumValue}}', exclusiveMaximum: '必须小于最大值: {{exclusiveMaximumValue}}', multipleOf: function (error) { if ((1 / error.multipleOfValue) % 10 === 0) { const decimals = Math.log10(1 / error.multipleOfValue); return `必须有 ${decimals} 位或更少的小数位`; } else { return `必须为 ${error.multipleOfValue} 的倍数`; } }, minProperties: '项目数必须大于或者等于 {{minimumProperties}} (当前项目数: {{currentProperties}})', maxProperties: '项目数必须小于或者等于 {{maximumProperties}} (当前项目数: {{currentProperties}})', minItems: '项目数必须大于或者等于 {{minimumItems}} (当前项目数: {{currentItems}})', maxItems: '项目数必须小于或者等于 {{maximumItems}} (当前项目数: {{currentItems}})', uniqueItems: '所有项目必须是唯一的', // Note: No default error messages for 'type', 'const', 'enum', or 'dependencies' }; /** * '_executeValidators' utility function * * Validates a control against an array of validators, and returns * an array of the same length containing a combination of error messages * (from invalid validators) and null values (from valid validators) * * // { AbstractControl } control - control to validate * // { IValidatorFn[] } validators - array of validators * // { boolean } invert - invert? * // { PlainObject[] } - array of nulls and error message */ function _executeValidators(control, validators, invert = false) { return validators.map(validator => validator(control, invert)); } /** * '_executeAsyncValidators' utility function * * Validates a control against an array of async validators, and returns * an array of observabe results of the same length containing a combination of * error messages (from invalid validators) and null values (from valid ones) * * // { AbstractControl } control - control to validate * // { AsyncIValidatorFn[] } validators - array of async validators * // { boolean } invert - invert? * // - array of observable nulls and error message */ function _executeAsyncValidators(control, validators, invert = false) { return validators.map(validator => validator(control, invert)); } /** * '_mergeObjects' utility function * * Recursively Merges one or more objects into a single object with combined keys. * Automatically detects and ignores null and undefined inputs. * Also detects duplicated boolean 'not' keys and XORs their values. * * // { PlainObject[] } objects - one or more objects to merge * // { PlainObject } - merged object */ function _mergeObjects(...objects) { const mergedObject = {}; for (const currentObject of objects) { if (isObject(currentObject)) { for (const key of Object.keys(currentObject)) { const currentValue = currentObject[key]; const mergedValue = mergedObject[key]; mergedObject[key] = !isDefined(mergedValue) ? currentValue : key === 'not' && isBoolean(mergedValue, 'strict') && isBoolean(currentValue, 'strict') ? xor(mergedValue, currentValue) : getType(mergedValue) === 'object' && getType(currentValue) === 'object' ? _mergeObjects(mergedValue, currentValue) : currentValue; } } } return mergedObject; } /** * '_mergeErrors' utility function * * Merges an array of objects. * Used for combining the validator errors returned from 'executeValidators' * * // { PlainObject[] } arrayOfErrors - array of objects * // { PlainObject } - merged object, or null if no usable input objectcs */ function _mergeErrors(arrayOfErrors) { const mergedErrors = _mergeObjects(...arrayOfErrors); return isEmpty(mergedErrors) ? null : mergedErrors; } /** * 'isDefined' utility function * * Checks if a variable contains a value of any type. * Returns true even for otherwise 'falsey' values of 0, '', and false. * * // value - the value to check * // { boolean } - false if undefined or null, otherwise true */ function isDefined(value) { return value !== undefined && value !== null; } /** * 'hasValue' utility function * * Checks if a variable contains a value. * Returs false for null, undefined, or a zero-length strng, '', * otherwise returns true. * (Stricter than 'isDefined' because it also returns false for '', * though it stil returns true for otherwise 'falsey' values 0 and false.) * * // value - the value to check * // { boolean } - false if undefined, null, or '', otherwise true */ function hasValue(value) { return value !== undefined && value !== null && value !== ''; } /** * 'isEmpty' utility function * * Similar to !hasValue, but also returns true for empty arrays and objects. * * // value - the value to check * // { boolean } - false if undefined, null, or '', otherwise true */ function isEmpty(value) { if (isArray(value)) { return !value.length; } if (isObject(value)) { return !Object.keys(value).length; } return value === undefined || value === null || value === ''; } /** * 'isString' utility function * * Checks if a value is a string. * * // value - the value to check * // { boolean } - true if string, false if not */ function isString(value) { return typeof value === 'string'; } /** * 'isNumber' utility function * * Checks if a value is a regular number, numeric string, or JavaScript Date. * * // value - the value to check * // { any = false } strict - if truthy, also checks JavaScript tyoe * // { boolean } - true if number, false if not */ function isNumber(value, strict = false) { if (strict && typeof value !== 'number') { return false; } return !isNaN(value) && value !== value / 0; } /** * 'isInteger' utility function * * Checks if a value is an integer. * * // value - the value to check * // { any = false } strict - if truthy, also checks JavaScript tyoe * // {boolean } - true if number, false if not */ function isInteger(value, strict = false) { if (strict && typeof value !== 'number') { return false; } return !isNaN(value) && value !== value / 0 && value % 1 === 0; } /** * 'isBoolean' utility function * * Checks if a value is a boolean. * * // value - the value to check * // { any = null } option - if 'strict', also checks JavaScript type * if TRUE or FALSE, checks only for that value * // { boolean } - true if boolean, false if not */ function isBoolean(value, option = null) { if (option === 'strict') { return value === true || value === false; } if (option === true) { return value === true || value === 1 || value === 'true' || value === '1'; } if (option === false) { return value === false || value === 0 || value === 'false' || value === '0'; } return value === true || value === 1 || value === 'true' || value === '1' || value === false || value === 0 || value === 'false' || value === '0'; } function isFunction(item) { return typeof item === 'function'; } function isObject(item) { return item !== null && typeof item === 'object'; } function isArray(item) { return Array.isArray(item); } function isDate(item) { return !!item && Object.prototype.toString.call(item) === '[object Date]'; } function isMap(item) { return !!item && Object.prototype.toString.call(item) === '[object Map]'; } function isSet(item) { return !!item && Object.prototype.toString.call(item) === '[object Set]'; } function isSymbol(item) { return typeof item === 'symbol'; } /** * 'getType' function * * Detects the JSON Schema Type of a value. * By default, detects numbers and integers even if formatted as strings. * (So all integers are also numbers, and any number may also be a string.) * However, it only detects true boolean values (to detect boolean values * in non-boolean formats, use isBoolean() instead). * * If passed a second optional parameter of 'strict', it will only detect * numbers and integers if they are formatted as JavaScript numbers. * * Examples: * getType('10.5') = 'number' * getType(10.5) = 'number' * getType('10') = 'integer' * getType(10) = 'integer' * getType('true') = 'string' * getType(true) = 'boolean' * getType(null) = 'null' * getType({ }) = 'object' * getType([]) = 'array' * * getType('10.5', 'strict') = 'string' * getType(10.5, 'strict') = 'number' * getType('10', 'strict') = 'string' * getType(10, 'strict') = 'integer' * getType('true', 'strict') = 'string' * getType(true, 'strict') = 'boolean' * * // value - value to check * // { any = false } strict - if truthy, also checks JavaScript tyoe * // { SchemaType } */ function getType(value, strict = false) { if (!isDefined(value)) { return 'null'; } if (isArray(value)) { return 'array'; } if (isObject(value)) { return 'object'; } if (isBoolean(value, 'strict')) { return 'boolean'; } if (isInteger(value, strict)) { return 'integer'; } if (isNumber(value, strict)) { return 'number'; } if (isString(value) || (!strict && isDate(value))) { return 'string'; } return null; } /** * 'isType' function * * Checks wether an input (probably string) value contains data of * a specified JSON Schema type * * // { PrimitiveValue } value - value to check * // { SchemaPrimitiveType } type - type to check * // { boolean } */ function isType(value, type) { switch (type) { case 'string': return isString(value) || isDate(value); case 'number': return isNumber(value); case 'integer': return isInteger(value); case 'boolean': return isBoolean(value); case 'null': return !hasValue(value); default: console.error(`isType error: "${type}" is not a recognized type.`); return null; } } /** * 'isPrimitive' function * * Checks wether an input value is a JavaScript primitive type: * string, number, boolean, or null. * * // value - value to check * // { boolean } */ function isPrimitive(value) { return (isString(value) || isNumber(value) || isBoolean(value, 'strict') || value === null); } /** * * @param date * @returns {string} * exmaple: * toDateString('2018-01-01') = '2018-01-01' * toDateString('2018-01-30T00:00:00.000Z') = '2018-01-30' */ const toIsoString = (date) => { const day = date.getDate(); const month = date.getMonth() + 1; const year = date.getFullYear(); return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`; }; /** * 'toJavaScriptType' function * * Converts an input (probably string) value to a JavaScript primitive type - * 'string', 'number', 'boolean', or 'null' - before storing in a JSON object. * * Does not coerce values (other than null), and only converts the types * of values that would otherwise be valid. * * If the optional third parameter 'strictIntegers' is TRUE, and the * JSON Schema type 'integer' is specified, it also verifies the input value * is an integer and, if it is, returns it as a JaveScript number. * If 'strictIntegers' is FALSE (or not set) the type 'integer' is treated * exactly the same as 'number', and allows decimals. * * Valid Examples: * toJavaScriptType('10', 'number' ) = 10 // '10' is a number * toJavaScriptType('10', 'integer') = 10 // '10' is also an integer * toJavaScriptType( 10, 'integer') = 10 // 10 is still an integer * toJavaScriptType( 10, 'string' ) = '10' // 10 can be made into a string * toJavaScriptType('10.5', 'number' ) = 10.5 // '10.5' is a number * * Invalid Examples: * toJavaScriptType('10.5', 'integer') = null // '10.5' is not an integer * toJavaScriptType( 10.5, 'integer') = null // 10.5 is still not an integer * * // { PrimitiveValue } value - value to convert * // { SchemaPrimitiveType | SchemaPrimitiveType[] } types - types to convert to * // { boolean = false } strictIntegers - if FALSE, treat integers as numbers * // { PrimitiveValue } */ function toJavaScriptType(value, types, strictIntegers = true) { if (!isDefined(value)) { return null; } if (isString(types)) { types = [types]; } if (strictIntegers && inArray('integer', types)) { if (isInteger(value, 'strict')) { return value; } if (isInteger(value)) { return parseInt(value, 10); } } if (inArray('number', types) || (!strictIntegers && inArray('integer', types))) { if (isNumber(value, 'strict')) { return value; } if (isNumber(value)) { return parseFloat(value); } } if (inArray('string', types)) { if (isString(value)) { return value; } // If value is a date, and types includes 'string', // convert the date to a string if (isDate(value)) { return toIsoString(value); } if (isNumber(value)) { return value.toString(); } } // If value is a date, and types includes 'integer' or 'number', // but not 'string', convert the date to a number if (isDate(value) && (inArray('integer', types) || inArray('number', types))) { return value.getTime(); } if (inArray('boolean', types)) { if (isBoolean(value, true)) { return true; } if (isBoolean(value, false)) { return false; } } return null; } /** * 'toSchemaType' function * * Converts an input (probably string) value to the "best" JavaScript * equivalent available from an allowed list of JSON Schema types, which may * contain 'string', 'number', 'integer', 'boolean', and/or 'null'. * If necssary, it does progressively agressive type coersion. * It will not return null unless null is in the list of allowed types. * * Number conversion examples: * toSchemaType('10', ['number','integer','string']) = 10 // integer * toSchemaType('10', ['number','string']) = 10 // number * toSchemaType('10', ['string']) = '10' // string * toSchemaType('10.5', ['number','integer','string']) = 10.5 // number * toSchemaType('10.5', ['integer','string']) = '10.5' // string * toSchemaType('10.5', ['integer']) = 10 // integer * toSchemaType(10.5, ['null','boolean','string']) = '10.5' // string * toSchemaType(10.5, ['null','boolean']) = true // boolean * * String conversion examples: * toSchemaType('1.5x', ['boolean','number','integer','string']) = '1.5x' // string * toSchemaType('1.5x', ['boolean','number','integer']) = '1.5' // number * toSchemaType('1.5x', ['boolean','integer']) = '1' // integer * toSchemaType('1.5x', ['boolean']) = true // boolean * toSchemaType('xyz', ['number','integer','boolean','null']) = true // boolean * toSchemaType('xyz', ['number','integer','null']) = null // null * toSchemaType('xyz', ['number','integer']) = 0 // number * * Boolean conversion examples: * toSchemaType('1', ['integer','number','string','boolean']) = 1 // integer * toSchemaType('1', ['number','string','boolean']) = 1 // number * toSchemaType('1', ['string','boolean']) = '1' // string * toSchemaType('1', ['boolean']) = true // boolean * toSchemaType('true', ['number','string','boolean']) = 'true' // string * toSchemaType('true', ['boolean']) = true // boolean * toSchemaType('true', ['number']) = 0 // number * toSchemaType(true, ['number','string','boolean']) = true // boolean * toSchemaType(true, ['number','string']) = 'true' // string * toSchemaType(true, ['number']) = 1 // number * * // { PrimitiveValue } value - value to convert * // { SchemaPrimitiveType | SchemaPrimitiveType[] } types - allowed types to convert to * // { PrimitiveValue } */ function toSchemaType(value, types) { if (!isArray(types)) { types = [types]; } if (types.includes('null') && !hasValue(value)) { return null; } if (types.includes('boolean') && !isBoolean(value, 'strict')) { return value; } if (types.includes('integer')) { const testValue = toJavaScriptType(value, 'integer'); if (testValue !== null) { return +testValue; } } if (types.includes('number')) { const testValue = toJavaScriptType(value, 'number'); if (testValue !== null) { return +testValue; } } if ((isString(value) || isNumber(value, 'strict')) && types.includes('string')) { // Convert number to string return toJavaScriptType(value, 'string'); } if (types.includes('boolean') && isBoolean(value)) { return toJavaScriptType(value, 'boolean'); } if (types.includes('string')) { // Convert null & boolean to string if (value === null) { return ''; } const testValue = toJavaScriptType(value, 'string'); if (testValue !== null) { return testValue; } } if ((types.includes('number') || types.includes('integer'))) { if (value === true) { return 1; } // Convert boolean & null to number if (value === false || value === null || value === '') { return 0; } } if (types.includes('number')) { // Convert mixed string to number const testValue = parseFloat(value); if (!!testValue) { return testValue; } } if (types.includes('integer')) { // Convert string or number to integer const testValue = parseInt(value, 10); if (!!testValue) { return testValue; } } if (types.includes('boolean')) { // Convert anything to boolean return !!value; } if ((types.includes('number') || types.includes('integer')) && !types.includes('null')) { return 0; // If null not allowed, return 0 for non-convertable values } } /** * 'isPromise' function * * // object * // { boolean } */ function isPromise(object) { return !!object && typeof object.then === 'function'; } /** * 'isObservable' function * * // object * // { boolean } */ function isObservable(object) { return !!object && typeof object.subscribe === 'function'; } /** * '_toPromise' function * * // { object } object * // { Promise<any> } */ function _toPromise(object) { return isPromise(object) ? object : object.toPromise(); } /** * 'toObservable' function * * // { object } object * // { Observable<any> } */ function toObservable(object) { const observable = isPromise(object) ? from(object) : object; if (isObservable(observable)) { return observable; } console.error('toObservable error: Expected validator to return Promise or Observable.'); return new Observable(); } /** * 'inArray' function * * Searches an array for an item, or one of a list of items, and returns true * as soon as a match is found, or false if no match. * * If the optional third parameter allIn is set to TRUE, and the item to find * is an array, then the function returns true only if all elements from item * are found in the array list, and false if any element is not found. If the * item to find is not an array, setting allIn to TRUE has no effect. * * // { any|any[] } item - the item to search for * // array - the array to search * // { boolean = false } allIn - if TRUE, all items must be in array * // { boolean } - true if item(s) in array, false otherwise */ function inArray(item, array, allIn = false) { if (!isDefined(item) || !isArray(array)) { return false; } return isArray(item) ? item[allIn ? 'every' : 'some'](subItem => array.includes(subItem)) : array.includes(item); } /** * 'xor' utility function - exclusive or * * Returns true if exactly one of two values is truthy. * * // value1 - first value to check * // value2 - second value to check * // { boolean } - true if exactly one input value is truthy, false if not */ function xor(value1, value2) { return (!!value1 && !value2) || (!value1 && !!value2); } /** * Utility function library: * * addClasses, copy, forEach, forEachCopy, hasOwn, mergeFilteredObject, * uniqueItems, commonItems, fixTitle, toTitleCase */ /** * 'addClasses' function * * Merges two space-delimited lists of CSS classes and removes duplicates. * * // {string | string[] | Set<string>} oldClasses * // {string | string[] | Set<string>} newClasses * // {string | string[] | Set<string>} - Combined classes */ function addClasses(oldClasses, newClasses) { const badType = i => !isSet(i) && !isArray(i) && !isString(i); if (badType(newClasses)) { return oldClasses; } if (badType(oldClasses)) { oldClasses = ''; } const toSet = i => isSet(i) ? i : isArray(i) ? new Set(i) : new Set(i.split(' ')); const combinedSet = toSet(oldClasses); const newSet = toSet(newClasses); newSet.forEach(c => combinedSet.add(c)); if (isSet(oldClasses)) { return combinedSet; } if (isArray(oldClasses)) { return Array.from(combinedSet); } return Array.from(combinedSet).join(' '); } /** * 'copy' function * * Makes a shallow copy of a JavaScript object, array, Map, or Set. * If passed a JavaScript primitive value (string, number, boolean, or null), * it returns the value. * * // {Object|Array|string|number|boolean|null} object - The object to copy * // {boolean = false} errors - Show errors? * // {Object|Array|string|number|boolean|null} - The copied object */ function copy(object, errors = false) { if (typeof object !== 'object' || object === null) { return object; } if (isMap(object)) { return new Map(object); } if (isSet(object)) { return new Set(object); } if (isArray(object)) { return [...object]; } if (isObject(object)) { return { ...object }; } if (errors) { console.error('copy error: Object to copy must be a JavaScript object or value.'); } return object; } /** * 'forEach' function * * Iterates over all items in the first level of an object or array * and calls an iterator funciton on each item. * * The iterator function is called with four values: * 1. The current item's value * 2. The current item's key * 3. The parent object, which contains the current item * 4. The root object * * Setting the optional third parameter to 'top-down' or 'bottom-up' will cause * it to also recursively iterate over items in sub-objects or sub-arrays in the * specified direction. * * // {Object|Array} object - The object or array to iterate over * // {function} fn - the iterator funciton to call on each item * // {boolean = false} errors - Show errors? * // {void} */ function forEach(object, fn, recurse = false, rootObject = object, errors = false) { if (isEmpty(object)) { return; } if ((isObject(object) || isArray(object)) && typeof fn === 'function') { for (const key of Object.keys(object)) { const value = object[key]; if (recurse === 'bottom-up' && (isObject(value) || isArray(value))) { forEach(value, fn, recurse, rootObject); } fn(value, key, object, rootObject); if (recurse === 'top-down' && (isObject(value) || isArray(value))) { forEach(value, fn, recurse, rootObject); } } } if (errors) { if (typeof fn !== 'function') { console.error('forEach error: Iterator must be a function.'); console.error('function', fn); } if (!isObject(object) && !isArray(object)) { console.error('forEach error: Input object must be an object or array.'); console.error('object', object); } } } /** * 'forEachCopy' function * * Iterates over all items in the first level of an object or array * and calls an iterator function on each item. Returns a new object or array * with the same keys or indexes as the original, and values set to the results * of the iterator function. * * Does NOT recursively iterate over items in sub-objects or sub-arrays. * * // {Object | Array} object - The object or array to iterate over * // {function} fn - The iterator funciton to call on each item * // {boolean = false} errors - Show errors? * // {Object | Array} - The resulting object or array */ function forEachCopy(object, fn, errors = false) { if (!hasValue(object)) { return; } if ((isObject(object) || isArray(object)) && typeof object !== 'function') { const newObject = isArray(object) ? [] : {}; for (const key of Object.keys(object)) { newObject[key] = fn(object[key], key, object); } return newObject; } if (errors) { if (typeof fn !== 'function') { console.error('forEachCopy error: Iterator must be a function.'); console.error('function', fn); } if (!isObject(object) && !isArray(object)) { console.error('forEachCopy error: Input object must be an object or array.'); console.error('object', object); } } } /** * 'hasOwn' utility function * * Checks whether an object or array has a particular property. * * // {any} object - the object to check * // {string} property - the property to look for * // {boolean} - true if object has property, false if not */ function hasOwn(object, property) { if (!