ngx-t-forms 2.0.29 → 2.0.31
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/fesm2022/ngx-t-forms-auto-complete-input-element.component-DCKuXHAW.mjs +104 -0
- package/fesm2022/ngx-t-forms-auto-complete-input-element.component-DCKuXHAW.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-basic-input-input-element.component-Ce4ipSUc.mjs +85 -0
- package/fesm2022/ngx-t-forms-basic-input-input-element.component-Ce4ipSUc.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-calculated-field-rules.component-C5TPddVe.mjs +643 -0
- package/fesm2022/ngx-t-forms-calculated-field-rules.component-C5TPddVe.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-chip-options-creator-editor.component-CICQaqz6.mjs +97 -0
- package/fesm2022/ngx-t-forms-chip-options-creator-editor.component-CICQaqz6.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-config-mscoa-additional-inputs.component-CzisLSIP.mjs +195 -0
- package/fesm2022/ngx-t-forms-config-mscoa-additional-inputs.component-CzisLSIP.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-data-source-picker.component-Dzz_o6fJ.mjs +261 -0
- package/fesm2022/ngx-t-forms-data-source-picker.component-Dzz_o6fJ.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-date-picker-input-element.component-CYUbVyzP.mjs +85 -0
- package/fesm2022/ngx-t-forms-date-picker-input-element.component-CYUbVyzP.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-date-range-picker-input-element.component-CmoquQGV.mjs +156 -0
- package/fesm2022/ngx-t-forms-date-range-picker-input-element.component-CmoquQGV.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-document-list-label-config-editor.component-CLUOXreG.mjs +368 -0
- package/fesm2022/ngx-t-forms-document-list-label-config-editor.component-CLUOXreG.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-document-picker.component-qObjcqhE.mjs +704 -0
- package/fesm2022/ngx-t-forms-document-picker.component-qObjcqhE.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-editor-input-element.component-BLXlfb6F.mjs +294 -0
- package/fesm2022/ngx-t-forms-editor-input-element.component-BLXlfb6F.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-editor-js-input.component-BQL0AH7H.mjs +240 -0
- package/fesm2022/ngx-t-forms-editor-js-input.component-BQL0AH7H.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-file-upload-input-element.component-C7mMeEjF.mjs +205 -0
- package/fesm2022/ngx-t-forms-file-upload-input-element.component-C7mMeEjF.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-form-input-selector.component-C9u8zq9B.mjs +86 -0
- package/fesm2022/ngx-t-forms-form-input-selector.component-C9u8zq9B.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-form-json-view.component-856Hx1Bg.mjs +22 -0
- package/fesm2022/ngx-t-forms-form-json-view.component-856Hx1Bg.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-form-payload-projection.component-CDkTuX9S.mjs +179 -0
- package/fesm2022/ngx-t-forms-form-payload-projection.component-CDkTuX9S.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-form-section-stepper.component-Bs50-nEB.mjs +319 -0
- package/fesm2022/ngx-t-forms-form-section-stepper.component-Bs50-nEB.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-forms-builder-menu.component-qrhM0jGL.mjs +379 -0
- package/fesm2022/ngx-t-forms-forms-builder-menu.component-qrhM0jGL.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-geo-location.component-Bosp1UzR.mjs +124 -0
- package/fesm2022/ngx-t-forms-geo-location.component-Bosp1UzR.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-getInputIcon-B4ADgevZ.mjs +31 -0
- package/fesm2022/ngx-t-forms-getInputIcon-B4ADgevZ.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-image-capture-input-element.component-C1g7Z0cK.mjs +180 -0
- package/fesm2022/ngx-t-forms-image-capture-input-element.component-C1g7Z0cK.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-index-dDSobs6A.mjs +2 -0
- package/fesm2022/ngx-t-forms-index-dDSobs6A.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-input-custom.component-BkbHFAyR.mjs +105 -0
- package/fesm2022/ngx-t-forms-input-custom.component-BkbHFAyR.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-input-editor.component-BPUOM9kQ.mjs +181 -0
- package/fesm2022/ngx-t-forms-input-editor.component-BPUOM9kQ.mjs.map +1 -0
- package/fesm2022/{ngx-t-forms-map-mat-options-keys-CbdW82su.mjs → ngx-t-forms-map-mat-options-keys-B6hJ7Io5.mjs} +12 -14
- package/fesm2022/ngx-t-forms-map-mat-options-keys-B6hJ7Io5.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-mat-chip-list-editor.component-c7uZT1sr.mjs +66 -0
- package/fesm2022/ngx-t-forms-mat-chip-list-editor.component-c7uZT1sr.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-mat-slider-editor.component-CTSBrM-j.mjs +211 -0
- package/fesm2022/ngx-t-forms-mat-slider-editor.component-CTSBrM-j.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-mat-slider-toggle-editor.component-CcYiwx-8.mjs +165 -0
- package/fesm2022/ngx-t-forms-mat-slider-toggle-editor.component-CcYiwx-8.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-missing-form-configs.component-DrnH8qdG.mjs +38 -0
- package/fesm2022/ngx-t-forms-missing-form-configs.component-DrnH8qdG.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-mscoa-chart-toolbar.component-C_abEBQ5.mjs +38 -0
- package/fesm2022/ngx-t-forms-mscoa-chart-toolbar.component-C_abEBQ5.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-mscoa-error-display.component-99DpVSy7.mjs +126 -0
- package/fesm2022/ngx-t-forms-mscoa-error-display.component-99DpVSy7.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-mscoa-segment-config.component-C0qsMfsq.mjs +336 -0
- package/fesm2022/ngx-t-forms-mscoa-segment-config.component-C0qsMfsq.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-mscoa-temporary-hint.component-B1Z-IXSL.mjs +74 -0
- package/fesm2022/ngx-t-forms-mscoa-temporary-hint.component-B1Z-IXSL.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-multiple-input-input-element.component-C7y1OGPx.mjs +905 -0
- package/fesm2022/ngx-t-forms-multiple-input-input-element.component-C7y1OGPx.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-ngx-t-forms-u_kigDid.mjs +19461 -0
- package/fesm2022/ngx-t-forms-ngx-t-forms-u_kigDid.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-paginated-selection-table-AQZSMmhr.mjs +555 -0
- package/fesm2022/ngx-t-forms-paginated-selection-table-AQZSMmhr.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-pipeline-generator.component-DmNSc5aw.mjs +748 -0
- package/fesm2022/ngx-t-forms-pipeline-generator.component-DmNSc5aw.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-record-list-manager.component-CUMMvMch.mjs +358 -0
- package/fesm2022/ngx-t-forms-record-list-manager.component-CUMMvMch.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-required-inputs.component-Ch2yNcIS.mjs +272 -0
- package/fesm2022/ngx-t-forms-required-inputs.component-Ch2yNcIS.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-rest-api-call-setup.component-C_aFtdvW.mjs +398 -0
- package/fesm2022/ngx-t-forms-rest-api-call-setup.component-C_aFtdvW.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-search-field.component-B2ZO7lqO.mjs +38 -0
- package/fesm2022/ngx-t-forms-search-field.component-B2ZO7lqO.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-section-report.component-BxOhR6C0.mjs +98 -0
- package/fesm2022/ngx-t-forms-section-report.component-BxOhR6C0.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-select-input-element.component-DbgZdNoe.mjs +150 -0
- package/fesm2022/ngx-t-forms-select-input-element.component-DbgZdNoe.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-selection-options-editor.component-Dhln81DL.mjs +169 -0
- package/fesm2022/ngx-t-forms-selection-options-editor.component-Dhln81DL.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-t-workflow-picker.component-leBokXvM.mjs +204 -0
- package/fesm2022/ngx-t-forms-t-workflow-picker.component-leBokXvM.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-textarea-input-element.component-BEbXJjFA.mjs +95 -0
- package/fesm2022/ngx-t-forms-textarea-input-element.component-BEbXJjFA.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-toggle-input-element.component-DDErRUJd.mjs +82 -0
- package/fesm2022/ngx-t-forms-toggle-input-element.component-DDErRUJd.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-validators-config.component-oGjQVGE2.mjs +733 -0
- package/fesm2022/ngx-t-forms-validators-config.component-oGjQVGE2.mjs.map +1 -0
- package/fesm2022/ngx-t-forms-workflow-adjudication.component-CtU8dECN.mjs +1303 -0
- package/fesm2022/ngx-t-forms-workflow-adjudication.component-CtU8dECN.mjs.map +1 -0
- package/fesm2022/ngx-t-forms.mjs +2 -1
- package/fesm2022/ngx-t-forms.mjs.map +1 -1
- package/package.json +20 -18
- package/styles/_editor-mixins.scss +62 -0
- package/styles/_json-editor-syntax.scss +26 -0
- package/styles/_signature-pad.scss +26 -0
- package/styles/_tokens.scss +148 -0
- package/types/ngx-t-forms.d.ts +1767 -621
- package/fesm2022/ngx-t-forms-calculated-field-rules.component-D-SBMdYg.mjs +0 -313
- package/fesm2022/ngx-t-forms-calculated-field-rules.component-D-SBMdYg.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-chip-options-creator-editor.component-1cpszpPN.mjs +0 -191
- package/fesm2022/ngx-t-forms-chip-options-creator-editor.component-1cpszpPN.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-config-mscoa-additional-inputs.component-DFdAVWTg.mjs +0 -207
- package/fesm2022/ngx-t-forms-config-mscoa-additional-inputs.component-DFdAVWTg.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-data-source-picker.component-DxORinAD.mjs +0 -204
- package/fesm2022/ngx-t-forms-data-source-picker.component-DxORinAD.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-document-list-label-config-editor.component-DcWS1txl.mjs +0 -289
- package/fesm2022/ngx-t-forms-document-list-label-config-editor.component-DcWS1txl.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-form-input-selector.component-B2QEnvkq.mjs +0 -134
- package/fesm2022/ngx-t-forms-form-input-selector.component-B2QEnvkq.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-form-json-view.component-DePf44w6.mjs +0 -22
- package/fesm2022/ngx-t-forms-form-json-view.component-DePf44w6.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-form-section-stepper.component-BTkcSjg7.mjs +0 -270
- package/fesm2022/ngx-t-forms-form-section-stepper.component-BTkcSjg7.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-forms-builder-menu.component-Wamzf_sq.mjs +0 -345
- package/fesm2022/ngx-t-forms-forms-builder-menu.component-Wamzf_sq.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-input-editor.component-D4xHO76K.mjs +0 -147
- package/fesm2022/ngx-t-forms-input-editor.component-D4xHO76K.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-map-mat-options-keys-CbdW82su.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-mat-chip-list-editor.component-DmTyO9Wi.mjs +0 -105
- package/fesm2022/ngx-t-forms-mat-chip-list-editor.component-DmTyO9Wi.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-mat-slider-editor.component-DZ4TenrI.mjs +0 -109
- package/fesm2022/ngx-t-forms-mat-slider-editor.component-DZ4TenrI.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-mat-slider-toggle-editor.component-DPyBYE4p.mjs +0 -155
- package/fesm2022/ngx-t-forms-mat-slider-toggle-editor.component-DPyBYE4p.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-missing-form-configs.component-BRmnwAK6.mjs +0 -28
- package/fesm2022/ngx-t-forms-missing-form-configs.component-BRmnwAK6.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-mscoa-chart-toolbar.component-D_umeAPL.mjs +0 -43
- package/fesm2022/ngx-t-forms-mscoa-chart-toolbar.component-D_umeAPL.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-mscoa-error-display.component-CSX2NCNU.mjs +0 -116
- package/fesm2022/ngx-t-forms-mscoa-error-display.component-CSX2NCNU.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-mscoa-segment-config.component-B6IF8kGg.mjs +0 -296
- package/fesm2022/ngx-t-forms-mscoa-segment-config.component-B6IF8kGg.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-mscoa-temporary-hint.component-BPkjsRmH.mjs +0 -83
- package/fesm2022/ngx-t-forms-mscoa-temporary-hint.component-BPkjsRmH.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-ngx-t-forms-D9qmig6g.mjs +0 -16844
- package/fesm2022/ngx-t-forms-ngx-t-forms-D9qmig6g.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-pipeline-generator.component-DBJEyCbd.mjs +0 -613
- package/fesm2022/ngx-t-forms-pipeline-generator.component-DBJEyCbd.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-record-list-manager.component-Dgs9lNSr.mjs +0 -269
- package/fesm2022/ngx-t-forms-record-list-manager.component-Dgs9lNSr.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-required-inputs.component-CSIJvSHq.mjs +0 -190
- package/fesm2022/ngx-t-forms-required-inputs.component-CSIJvSHq.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-rest-api-call-setup.component-CY-JSkGs.mjs +0 -291
- package/fesm2022/ngx-t-forms-rest-api-call-setup.component-CY-JSkGs.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-section-report.component-12-KdKT6.mjs +0 -156
- package/fesm2022/ngx-t-forms-section-report.component-12-KdKT6.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-selection-options-editor.component-Be3QAG_L.mjs +0 -186
- package/fesm2022/ngx-t-forms-selection-options-editor.component-Be3QAG_L.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-t-workflow-picker.component-a4f1r8gH.mjs +0 -187
- package/fesm2022/ngx-t-forms-t-workflow-picker.component-a4f1r8gH.mjs.map +0 -1
- package/fesm2022/ngx-t-forms-validators-config.component-B3j9Dmgu.mjs +0 -215
- package/fesm2022/ngx-t-forms-validators-config.component-B3j9Dmgu.mjs.map +0 -1
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, output, inject, ElementRef, signal, computed, Input, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { v4 } from 'uuid';
|
|
4
|
+
import { CalculationFunctions, InputDataTypes, InputTypes } from 'ngx-t-forms-types';
|
|
5
|
+
import * as i3$2 from '@angular/common';
|
|
6
|
+
import { CommonModule } from '@angular/common';
|
|
7
|
+
import { NgControl } from '@angular/forms';
|
|
8
|
+
import { MatFormFieldControl } from '@angular/material/form-field';
|
|
9
|
+
import * as i3 from '@angular/material/icon';
|
|
10
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
11
|
+
import * as i1 from '@angular/material/chips';
|
|
12
|
+
import { MatChipsModule } from '@angular/material/chips';
|
|
13
|
+
import * as i3$1 from '@angular/material/card';
|
|
14
|
+
import { MatCardModule } from '@angular/material/card';
|
|
15
|
+
import * as i5 from '@angular/material/toolbar';
|
|
16
|
+
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
17
|
+
import * as i1$1 from '@angular/material/button';
|
|
18
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
19
|
+
import * as i2 from '@angular/material/button-toggle';
|
|
20
|
+
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
|
21
|
+
import * as i1$2 from '@angular/material/slide-toggle';
|
|
22
|
+
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
23
|
+
import * as i5$1 from '@angular/material/tooltip';
|
|
24
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
25
|
+
import { Subject } from 'rxjs';
|
|
26
|
+
import { P as PREDICATE_OPERATORS, f as evaluatePredicate } from './ngx-t-forms-ngx-t-forms-u_kigDid.mjs';
|
|
27
|
+
|
|
28
|
+
/** Canonical operator value keyed by its lower-cased spelling (for parse normalisation). */
|
|
29
|
+
const OPERATOR_BY_KEY = new Map(PREDICATE_OPERATORS.map((op) => [op.value.toLowerCase(), op.value]));
|
|
30
|
+
/** Symbolic operators, longest-match first so `===` wins over `==`. */
|
|
31
|
+
const SYMBOLIC_OPS = ['===', '!==', '>=', '<=', '==', '!=', '>', '<'];
|
|
32
|
+
/** Keyword operators (matched case-insensitively, surrounded by whitespace). */
|
|
33
|
+
const KEYWORD_OPS = ['includes', 'startsWith', 'endsWith', 'matches', 'in'];
|
|
34
|
+
/** An empty condition seeded with the given field and a sensible default operator. */
|
|
35
|
+
function emptyCondition(field = '') {
|
|
36
|
+
return { field, operator: '===', compareTo: 'value', value: '' };
|
|
37
|
+
}
|
|
38
|
+
const isBareLiteral = (v) => v === 'true' || v === 'false' || v === 'null' || (v.trim() !== '' && !Number.isNaN(Number(v)));
|
|
39
|
+
const isQuoted = (v) => {
|
|
40
|
+
const t = v.trim();
|
|
41
|
+
if (t.length < 2)
|
|
42
|
+
return false;
|
|
43
|
+
const a = t[0];
|
|
44
|
+
const b = t[t.length - 1];
|
|
45
|
+
return (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
46
|
+
};
|
|
47
|
+
const unquote = (v) => (isQuoted(v) ? v.trim().slice(1, -1) : v.trim());
|
|
48
|
+
/** Formats a literal: bare for numbers/booleans/null, otherwise a quoted, escaped string. */
|
|
49
|
+
function formatLiteral(value) {
|
|
50
|
+
const t = value.trim();
|
|
51
|
+
if (t === '')
|
|
52
|
+
return '""';
|
|
53
|
+
if (isBareLiteral(t))
|
|
54
|
+
return t;
|
|
55
|
+
return `"${t.replace(/"/g, '\\"')}"`;
|
|
56
|
+
}
|
|
57
|
+
/** The expression token for a condition's right operand. */
|
|
58
|
+
function rightToken(condition) {
|
|
59
|
+
return condition.compareTo === 'field' ? condition.value.trim() : formatLiteral(condition.value);
|
|
60
|
+
}
|
|
61
|
+
/** Whether a condition is complete enough to contribute to the expression. */
|
|
62
|
+
function isComplete(condition) {
|
|
63
|
+
if (condition.field.trim() === '')
|
|
64
|
+
return false;
|
|
65
|
+
if (condition.compareTo === 'field')
|
|
66
|
+
return condition.value.trim() !== '';
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Serialises a condition group into a predicate expression string. Incomplete
|
|
71
|
+
* conditions are dropped, so an empty/half-built group yields `''` (which the
|
|
72
|
+
* editor treats as "nothing to save yet").
|
|
73
|
+
*/
|
|
74
|
+
function serializeConditions(group) {
|
|
75
|
+
return group.conditions
|
|
76
|
+
.filter(isComplete)
|
|
77
|
+
.map((c) => `${c.field.trim()} ${c.operator} ${rightToken(c)}`)
|
|
78
|
+
.join(` ${group.connector} `);
|
|
79
|
+
}
|
|
80
|
+
/** Resolves a raw operator spelling to its canonical {@link PREDICATE_OPERATORS} value, or `null`. */
|
|
81
|
+
function canonicalOperator(raw) {
|
|
82
|
+
return OPERATOR_BY_KEY.get(raw.toLowerCase()) ?? null;
|
|
83
|
+
}
|
|
84
|
+
/** Classifies a raw right-hand operand as a field reference or a literal value. */
|
|
85
|
+
function classifyRight(raw, fieldNames) {
|
|
86
|
+
const t = raw.trim();
|
|
87
|
+
if (isQuoted(t))
|
|
88
|
+
return { compareTo: 'value', value: unquote(t) };
|
|
89
|
+
if (fieldNames.has(t))
|
|
90
|
+
return { compareTo: 'field', value: t };
|
|
91
|
+
return { compareTo: 'value', value: t };
|
|
92
|
+
}
|
|
93
|
+
/** Parses a single `left op right` comparison, or `null` if it is not representable. */
|
|
94
|
+
function parseCondition(part, fieldNames) {
|
|
95
|
+
const p = part.trim();
|
|
96
|
+
for (const op of SYMBOLIC_OPS) {
|
|
97
|
+
const idx = p.indexOf(op);
|
|
98
|
+
if (idx > 0) {
|
|
99
|
+
const operator = canonicalOperator(op);
|
|
100
|
+
if (!operator)
|
|
101
|
+
return null;
|
|
102
|
+
const field = p.slice(0, idx).trim();
|
|
103
|
+
const right = p.slice(idx + op.length).trim();
|
|
104
|
+
if (field === '' || right === '')
|
|
105
|
+
return null;
|
|
106
|
+
return { field, operator, ...classifyRight(right, fieldNames) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const kw of KEYWORD_OPS) {
|
|
110
|
+
const m = p.match(new RegExp(`^(.+?)\\s+${kw}\\s+(.+)$`, 'i'));
|
|
111
|
+
if (m && m[1] && m[2]) {
|
|
112
|
+
const operator = canonicalOperator(kw);
|
|
113
|
+
if (!operator)
|
|
114
|
+
return null;
|
|
115
|
+
return { field: m[1].trim(), operator, ...classifyRight(m[2], fieldNames) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Best-effort parse of an expression into a guided condition group. Returns
|
|
122
|
+
* `null` (→ raw editor) for anything the builder cannot represent losslessly:
|
|
123
|
+
* grouping `()`, mixed `&&`/`||`, unary `!`, or an unknown operator.
|
|
124
|
+
*/
|
|
125
|
+
function parseConditions(expression, fieldNames) {
|
|
126
|
+
const e = expression.trim();
|
|
127
|
+
if (e === '')
|
|
128
|
+
return { connector: '&&', conditions: [] };
|
|
129
|
+
if (/[()]/.test(e))
|
|
130
|
+
return null; // grouping → raw
|
|
131
|
+
if (/!(?!=)/.test(e))
|
|
132
|
+
return null; // unary negation (`!field`) → raw
|
|
133
|
+
const hasAnd = e.includes('&&');
|
|
134
|
+
const hasOr = e.includes('||');
|
|
135
|
+
if (hasAnd && hasOr)
|
|
136
|
+
return null; // mixed connectors → raw
|
|
137
|
+
const connector = hasOr ? '||' : '&&';
|
|
138
|
+
const parts = hasOr ? e.split('||') : hasAnd ? e.split('&&') : [e];
|
|
139
|
+
const conditions = [];
|
|
140
|
+
for (const part of parts) {
|
|
141
|
+
const condition = parseCondition(part, fieldNames);
|
|
142
|
+
if (!condition)
|
|
143
|
+
return null;
|
|
144
|
+
conditions.push(condition);
|
|
145
|
+
}
|
|
146
|
+
return { connector, conditions };
|
|
147
|
+
}
|
|
148
|
+
/** Returns the unique identifier tokens referenced in an expression. */
|
|
149
|
+
function referencedTokens(expression) {
|
|
150
|
+
return new Set(expression.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Derives the `inputsObservedForChanges` dependency list from an expression by
|
|
154
|
+
* matching identifier tokens against the editor's resolved {@link FieldBinding}s.
|
|
155
|
+
* Works for both guided and raw expressions, so observed inputs always reflect
|
|
156
|
+
* what the expression actually references — no manual add/remove, no orphans.
|
|
157
|
+
*
|
|
158
|
+
* When an aggregate has been chosen for a field (only possible for a
|
|
159
|
+
* multiple-input column), the binding carries `parentInputId` + `function` so
|
|
160
|
+
* the engine reduces its row array to a scalar. Otherwise it resolves directly
|
|
161
|
+
* (a scalar primary input, or a per-row list value).
|
|
162
|
+
*/
|
|
163
|
+
function deriveObservedInputs(expression, fields, functions) {
|
|
164
|
+
const tokens = referencedTokens(expression);
|
|
165
|
+
const observed = [];
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
for (const field of fields) {
|
|
168
|
+
if (!tokens.has(field.variable) || seen.has(field.variable))
|
|
169
|
+
continue;
|
|
170
|
+
seen.add(field.variable);
|
|
171
|
+
const aggregate = functions[field.variable];
|
|
172
|
+
if (aggregate && field.parentInputId) {
|
|
173
|
+
observed.push({
|
|
174
|
+
inputId: field.inputId,
|
|
175
|
+
variable: field.variable,
|
|
176
|
+
parentInputId: field.parentInputId,
|
|
177
|
+
function: aggregate,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
observed.push({ inputId: field.inputId, variable: field.variable });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return observed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Internal editor implementing `MatFormFieldControl<FormControlCustomValidatorsInterface[]>`
|
|
189
|
+
* for authoring a form input's custom validators.
|
|
190
|
+
*
|
|
191
|
+
* Rendered by `t-dynamic-data-edit` inside its `<mat-form-field>`, which supplies
|
|
192
|
+
* the surrounding field label, hint, and validation errors — so this component
|
|
193
|
+
* owns only the validator chip list and the inline draft editor.
|
|
194
|
+
*
|
|
195
|
+
* ### Authoring model
|
|
196
|
+
* A validator's `expression` is evaluated by the shared predicate DSL where
|
|
197
|
+
* **`true` means the field is invalid** (the error message is then shown). The
|
|
198
|
+
* editor captures that condition through a **guided builder** — `field operator
|
|
199
|
+
* value` rows joined by All/Any — that serialises to the expression string and
|
|
200
|
+
* auto-derives `inputsObservedForChanges` from the fields it references. An
|
|
201
|
+
* **Advanced** raw editor is the escape hatch for expressions the builder cannot
|
|
202
|
+
* represent (grouping, mixed connectors, negation); these round-trip unchanged.
|
|
203
|
+
*
|
|
204
|
+
* The bound `value` may contain both string entries (legacy validator-name
|
|
205
|
+
* references such as `'required'`, which are read-only) and object entries
|
|
206
|
+
* ({@link FormControlCustomValidatorsInterface}, which are editable). Only object
|
|
207
|
+
* entries open the draft editor.
|
|
208
|
+
*
|
|
209
|
+
* NOTE: `MatFormFieldControl<T>` mandates plain mutable `value` / `disabled` /
|
|
210
|
+
* `required` / `placeholder` / `id` members, `@Input()` decorators, the
|
|
211
|
+
* `focused` / `touched` / `stateChanges` members, and the `useExisting`
|
|
212
|
+
* provider. These are framework wiring, not defects — the CLAUDE.md bans on
|
|
213
|
+
* `@Input()` / `useExisting` do not apply to MatFormFieldControl integration.
|
|
214
|
+
*/
|
|
215
|
+
class ValidatorsConfigComponent {
|
|
216
|
+
constructor() {
|
|
217
|
+
/** Emits whenever a piece of `MatFormFieldControl` state changes. */
|
|
218
|
+
this.stateChanges = new Subject();
|
|
219
|
+
this.controlType = 'lib-validators-config';
|
|
220
|
+
this.id = `lib-validators-config-${ValidatorsConfigComponent.nextId++}`;
|
|
221
|
+
this.placeholder = '';
|
|
222
|
+
this.focused = false;
|
|
223
|
+
this.touched = false;
|
|
224
|
+
this.required = false;
|
|
225
|
+
this.describedBy = '';
|
|
226
|
+
this.autofilled = undefined;
|
|
227
|
+
this.userAriaDescribedBy = undefined;
|
|
228
|
+
this.disableAutomaticLabeling = undefined;
|
|
229
|
+
/** Whether the editor is inert. Kept as `@Input()` for the MFC contract. */
|
|
230
|
+
this.disabled = false;
|
|
231
|
+
/** Current validator list. Kept as `@Input()` for the MFC contract. */
|
|
232
|
+
this.value = [];
|
|
233
|
+
/** Validation errors surfaced by the parent for error-state derivation. */
|
|
234
|
+
this.errors = [];
|
|
235
|
+
/** Source input that owns this validator config (for multiple-input scopes). */
|
|
236
|
+
this.mapToData = input(undefined, ...(ngDevMode ? [{ debugName: "mapToData" }] : /* istanbul ignore next */ []));
|
|
237
|
+
/** Available form inputs offered as variables. */
|
|
238
|
+
this.formInputs = input([], ...(ngDevMode ? [{ debugName: "formInputs" }] : /* istanbul ignore next */ []));
|
|
239
|
+
/** Emits the full validator list whenever it changes. */
|
|
240
|
+
this.valueChanged = output();
|
|
241
|
+
/** Operator choices shared with the array-access predicate builder. */
|
|
242
|
+
this.operators = PREDICATE_OPERATORS;
|
|
243
|
+
/** Aggregate choices for a list field, with non-technical labels. */
|
|
244
|
+
this.aggregates = [
|
|
245
|
+
{ value: CalculationFunctions.Sum, label: 'total' },
|
|
246
|
+
{ value: CalculationFunctions.Avg, label: 'average' },
|
|
247
|
+
{ value: CalculationFunctions.Min, label: 'lowest' },
|
|
248
|
+
{ value: CalculationFunctions.Max, label: 'highest' },
|
|
249
|
+
{ value: CalculationFunctions.Count, label: 'count' },
|
|
250
|
+
];
|
|
251
|
+
this.#elementRef = inject(ElementRef);
|
|
252
|
+
this.ngControl = inject(NgControl, { self: true, optional: true });
|
|
253
|
+
/** Draft currently open in the inline editor, or `null` when closed. */
|
|
254
|
+
this.#edit = signal(null, ...(ngDevMode ? [{ debugName: "#edit" }] : /* istanbul ignore next */ []));
|
|
255
|
+
/** Read-only view of the open draft for the template. */
|
|
256
|
+
this.edit = this.#edit.asReadonly();
|
|
257
|
+
/** Guided builder model mirroring the open draft's expression. */
|
|
258
|
+
this.#group = signal({ connector: '&&', conditions: [] }, ...(ngDevMode ? [{ debugName: "#group" }] : /* istanbul ignore next */ []));
|
|
259
|
+
this.group = this.#group.asReadonly();
|
|
260
|
+
/** Whether the draft is being edited as a raw expression instead of guided rows. */
|
|
261
|
+
this.#raw = signal(false, ...(ngDevMode ? [{ debugName: "#raw" }] : /* istanbul ignore next */ []));
|
|
262
|
+
this.raw = this.#raw.asReadonly();
|
|
263
|
+
/** Chosen aggregate per referenced list variable (keyed by variable name). */
|
|
264
|
+
this.#functions = signal({}, ...(ngDevMode ? [{ debugName: "#functions" }] : /* istanbul ignore next */ []));
|
|
265
|
+
/** Whether the validator being authored lives on a multiple-input sub-item. */
|
|
266
|
+
this.validatorOnList = computed(() => !!this.mapToData()?.multipleInputInEditId, ...(ngDevMode ? [{ debugName: "validatorOnList" }] : /* istanbul ignore next */ []));
|
|
267
|
+
/**
|
|
268
|
+
* Fields the builder can reference: every primary input plus every
|
|
269
|
+
* multiple-input column. A multiple column carries its row-array container
|
|
270
|
+
* (`parentInputId`); whether it must be aggregated is decided per condition
|
|
271
|
+
* (see {@link requiredAggregateVariables}), not by the field itself.
|
|
272
|
+
*/
|
|
273
|
+
this.fieldOptions = computed(() => {
|
|
274
|
+
const self = this.mapToData();
|
|
275
|
+
const selfId = self?.id;
|
|
276
|
+
const options = [];
|
|
277
|
+
for (const input of this.formInputs()) {
|
|
278
|
+
if (!input.formControlName)
|
|
279
|
+
continue;
|
|
280
|
+
const group = input.multipleInputInEditId;
|
|
281
|
+
options.push({
|
|
282
|
+
variable: input.formControlName,
|
|
283
|
+
label: input.label || input.formControlName,
|
|
284
|
+
isSelf: input.id === selfId,
|
|
285
|
+
isMultiple: !!group,
|
|
286
|
+
numeric: this.#isNumeric(input),
|
|
287
|
+
inputId: group ? input.id : (input.originalId ?? input.id),
|
|
288
|
+
parentInputId: group,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return options;
|
|
292
|
+
}, ...(ngDevMode ? [{ debugName: "fieldOptions" }] : /* istanbul ignore next */ []));
|
|
293
|
+
/** Primary (scalar) fields, for the first dropdown group. */
|
|
294
|
+
this.directFields = computed(() => this.fieldOptions().filter((f) => !f.isMultiple), ...(ngDevMode ? [{ debugName: "directFields" }] : /* istanbul ignore next */ []));
|
|
295
|
+
/** Multiple-input columns, for the second dropdown group. */
|
|
296
|
+
this.listFields = computed(() => this.fieldOptions().filter((f) => f.isMultiple), ...(ngDevMode ? [{ debugName: "listFields" }] : /* istanbul ignore next */ []));
|
|
297
|
+
/** Field option keyed by its expression variable, for operand lookups. */
|
|
298
|
+
this.#fieldByVariable = computed(() => new Map(this.fieldOptions().map((f) => [f.variable, f])), ...(ngDevMode ? [{ debugName: "#fieldByVariable" }] : /* istanbul ignore next */ []));
|
|
299
|
+
/** Multiple-input columns actually referenced by the current expression. */
|
|
300
|
+
this.referencedListFields = computed(() => {
|
|
301
|
+
const tokens = referencedTokens(this.#edit()?.expression ?? '');
|
|
302
|
+
return this.listFields().filter((f) => tokens.has(f.variable));
|
|
303
|
+
}, ...(ngDevMode ? [{ debugName: "referencedListFields" }] : /* istanbul ignore next */ []));
|
|
304
|
+
/**
|
|
305
|
+
* Multiple-input variables that MUST be aggregated: a list column compared
|
|
306
|
+
* against a non-list operand (a primary field or a typed value) has to be
|
|
307
|
+
* reduced to a single value. A list-vs-list comparison leaves the aggregate
|
|
308
|
+
* optional (the columns are row-aligned). In raw mode operand pairing cannot
|
|
309
|
+
* be analysed, so every referenced list column is treated as required.
|
|
310
|
+
*/
|
|
311
|
+
this.requiredAggregateVariables = computed(() => {
|
|
312
|
+
if (this.#raw()) {
|
|
313
|
+
return new Set(this.referencedListFields().map((f) => f.variable));
|
|
314
|
+
}
|
|
315
|
+
const byVariable = this.#fieldByVariable();
|
|
316
|
+
const required = new Set();
|
|
317
|
+
for (const cond of this.#group().conditions) {
|
|
318
|
+
const left = byVariable.get(cond.field);
|
|
319
|
+
const right = cond.compareTo === 'field' ? byVariable.get(cond.value) : undefined;
|
|
320
|
+
const leftMultiple = !!left?.isMultiple;
|
|
321
|
+
const rightMultiple = !!right?.isMultiple;
|
|
322
|
+
if (leftMultiple && !rightMultiple)
|
|
323
|
+
required.add(cond.field);
|
|
324
|
+
if (rightMultiple && !leftMultiple && right)
|
|
325
|
+
required.add(right.variable);
|
|
326
|
+
}
|
|
327
|
+
return required;
|
|
328
|
+
}, ...(ngDevMode ? [{ debugName: "requiredAggregateVariables" }] : /* istanbul ignore next */ []));
|
|
329
|
+
/** Whether every list column that MUST be aggregated has an aggregate chosen. */
|
|
330
|
+
this.aggregatesResolved = computed(() => {
|
|
331
|
+
const functions = this.#functions();
|
|
332
|
+
const required = this.requiredAggregateVariables();
|
|
333
|
+
return [...required].every((variable) => !!functions[variable]);
|
|
334
|
+
}, ...(ngDevMode ? [{ debugName: "aggregatesResolved" }] : /* istanbul ignore next */ []));
|
|
335
|
+
/** Whether the guided builder has at least one condition and all are complete. */
|
|
336
|
+
this.conditionsComplete = computed(() => {
|
|
337
|
+
if (this.#raw())
|
|
338
|
+
return true; // raw mode is gated by syntaxError + non-empty
|
|
339
|
+
const conditions = this.#group().conditions;
|
|
340
|
+
return conditions.length > 0 && conditions.every((c) => this.isConditionComplete(c));
|
|
341
|
+
}, ...(ngDevMode ? [{ debugName: "conditionsComplete" }] : /* istanbul ignore next */ []));
|
|
342
|
+
/** Whether the current draft expression can be edited in the guided builder. */
|
|
343
|
+
this.canUseGuided = computed(() => parseConditions(this.#edit()?.expression ?? '', this.#fieldNameSet()) !== null, ...(ngDevMode ? [{ debugName: "canUseGuided" }] : /* istanbul ignore next */ []));
|
|
344
|
+
/** Human-readable syntax error for the raw editor, or `null` when valid/empty. */
|
|
345
|
+
this.syntaxError = computed(() => {
|
|
346
|
+
const expression = this.#edit()?.expression ?? '';
|
|
347
|
+
if (expression.trim() === '')
|
|
348
|
+
return null;
|
|
349
|
+
try {
|
|
350
|
+
evaluatePredicate(expression, {});
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return error instanceof Error ? error.message : 'The expression syntax is invalid.';
|
|
355
|
+
}
|
|
356
|
+
}, ...(ngDevMode ? [{ debugName: "syntaxError" }] : /* istanbul ignore next */ []));
|
|
357
|
+
/** Whether the open draft can be saved (message, a complete/valid rule, aggregates chosen). */
|
|
358
|
+
this.canSave = computed(() => {
|
|
359
|
+
const draft = this.#edit();
|
|
360
|
+
return (!!draft &&
|
|
361
|
+
!!draft.message &&
|
|
362
|
+
!!draft.expression &&
|
|
363
|
+
this.syntaxError() === null &&
|
|
364
|
+
this.conditionsComplete() &&
|
|
365
|
+
this.aggregatesResolved());
|
|
366
|
+
}, ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
|
|
367
|
+
/** A one-line reason the rule cannot be saved yet, or `null` when it can. */
|
|
368
|
+
this.saveHint = computed(() => {
|
|
369
|
+
const draft = this.#edit();
|
|
370
|
+
if (!draft)
|
|
371
|
+
return null;
|
|
372
|
+
if (!draft.message)
|
|
373
|
+
return 'Add an error message.';
|
|
374
|
+
if (!this.#raw() && !this.conditionsComplete()) {
|
|
375
|
+
return 'Finish every condition — choose a field, a condition and a value.';
|
|
376
|
+
}
|
|
377
|
+
if (!draft.expression)
|
|
378
|
+
return 'Add at least one condition.';
|
|
379
|
+
if (this.#raw() && this.syntaxError() !== null)
|
|
380
|
+
return this.syntaxError();
|
|
381
|
+
if (!this.aggregatesResolved())
|
|
382
|
+
return 'Choose how to combine each list field.';
|
|
383
|
+
return null;
|
|
384
|
+
}, ...(ngDevMode ? [{ debugName: "saveHint" }] : /* istanbul ignore next */ []));
|
|
385
|
+
this.onTouched = () => { };
|
|
386
|
+
}
|
|
387
|
+
static { this.nextId = 0; }
|
|
388
|
+
#elementRef;
|
|
389
|
+
/** Draft currently open in the inline editor, or `null` when closed. */
|
|
390
|
+
#edit;
|
|
391
|
+
/** Guided builder model mirroring the open draft's expression. */
|
|
392
|
+
#group;
|
|
393
|
+
/** Whether the draft is being edited as a raw expression instead of guided rows. */
|
|
394
|
+
#raw;
|
|
395
|
+
/** Chosen aggregate per referenced list variable (keyed by variable name). */
|
|
396
|
+
#functions;
|
|
397
|
+
/** Field option keyed by its expression variable, for operand lookups. */
|
|
398
|
+
#fieldByVariable;
|
|
399
|
+
/** Aggregates offered for a field: numeric columns get all, others only count. */
|
|
400
|
+
aggregatesFor(field) {
|
|
401
|
+
return field.numeric
|
|
402
|
+
? this.aggregates
|
|
403
|
+
: this.aggregates.filter((a) => a.value === CalculationFunctions.Count);
|
|
404
|
+
}
|
|
405
|
+
/** Whether a referenced list column is required to have an aggregate. */
|
|
406
|
+
isAggregateRequired(variable) {
|
|
407
|
+
return this.requiredAggregateVariables().has(variable);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Whether a guided condition is fully specified (field, operator and a value
|
|
411
|
+
* all set). Incomplete conditions are otherwise silently dropped on save, so
|
|
412
|
+
* this gates {@link canSave}.
|
|
413
|
+
*/
|
|
414
|
+
isConditionComplete(condition) {
|
|
415
|
+
return (condition.field.trim() !== '' &&
|
|
416
|
+
condition.operator.trim() !== '' &&
|
|
417
|
+
condition.value.trim() !== '');
|
|
418
|
+
}
|
|
419
|
+
// MatFormFieldControl reads these as plain getters during its own change
|
|
420
|
+
// detection, so they stay getters rather than `computed()` signals.
|
|
421
|
+
get empty() {
|
|
422
|
+
return this.value.length === 0;
|
|
423
|
+
}
|
|
424
|
+
get shouldLabelFloat() {
|
|
425
|
+
return this.focused || !this.empty;
|
|
426
|
+
}
|
|
427
|
+
get errorState() {
|
|
428
|
+
const ngControlError = this.ngControl?.errors != null;
|
|
429
|
+
const externalError = (this.errors?.length ?? 0) > 0 && this.touched;
|
|
430
|
+
const requiredUnset = this.empty && this.required;
|
|
431
|
+
return requiredUnset || ngControlError || externalError;
|
|
432
|
+
}
|
|
433
|
+
setDescribedByIds(ids) {
|
|
434
|
+
this.describedBy = ids.join(' ');
|
|
435
|
+
const el = this.#elementRef.nativeElement;
|
|
436
|
+
const controlElement = el.querySelector('.lib-validators-config__list');
|
|
437
|
+
controlElement?.setAttribute('aria-describedby', this.describedBy);
|
|
438
|
+
this.stateChanges.next();
|
|
439
|
+
}
|
|
440
|
+
onContainerClick() {
|
|
441
|
+
this.markAsTouched();
|
|
442
|
+
this.stateChanges.next();
|
|
443
|
+
}
|
|
444
|
+
markAsTouched() {
|
|
445
|
+
if (!this.touched) {
|
|
446
|
+
this.touched = true;
|
|
447
|
+
this.onTouched();
|
|
448
|
+
this.stateChanges.next();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/** Tear down the MFC state stream so subscribers do not leak. */
|
|
452
|
+
ngOnDestroy() {
|
|
453
|
+
this.stateChanges.complete();
|
|
454
|
+
}
|
|
455
|
+
/** `@for` track key: object validators by id, string validators by value. */
|
|
456
|
+
trackValidator(validator) {
|
|
457
|
+
return typeof validator === 'string' ? validator : validator.id;
|
|
458
|
+
}
|
|
459
|
+
/** Resolve the label shown on a validator chip. */
|
|
460
|
+
validatorLabel(validator) {
|
|
461
|
+
return typeof validator === 'string' ? validator : validator.message;
|
|
462
|
+
}
|
|
463
|
+
/** Whether a validator chip is the one currently being edited. */
|
|
464
|
+
isEditing(validator) {
|
|
465
|
+
if (typeof validator === 'string') {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
return this.#edit()?.id === validator.id;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Open a validator for editing. String entries are read-only legacy
|
|
472
|
+
* references and are ignored. Editing the open validator again closes it.
|
|
473
|
+
* The draft is a clone, so edits only land in `value` on Save.
|
|
474
|
+
*
|
|
475
|
+
* DEFECT FIX (discard semantics): the editor is an inline panel with an
|
|
476
|
+
* explicit Close/Save toolbar, not a transient overlay that vanishes on an
|
|
477
|
+
* outside click. Opening a different validator (or {@link addValidator})
|
|
478
|
+
* deliberately replaces the open draft — an explicit, documented discard,
|
|
479
|
+
* never a silent loss mid-edit.
|
|
480
|
+
*/
|
|
481
|
+
editV(validator) {
|
|
482
|
+
if (typeof validator === 'string') {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (this.#edit()?.id === validator.id) {
|
|
486
|
+
this.#edit.set(null);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.#edit.set(this.#cloneValidator(validator));
|
|
490
|
+
this.#initBuilderFromDraft();
|
|
491
|
+
}
|
|
492
|
+
/** Begin a brand-new validator draft (id assigned on save). */
|
|
493
|
+
addValidator() {
|
|
494
|
+
this.#edit.set(this.#emptyDraft());
|
|
495
|
+
this.#initBuilderFromDraft();
|
|
496
|
+
}
|
|
497
|
+
/** Discard the open draft without persisting it. */
|
|
498
|
+
closeEditor() {
|
|
499
|
+
this.#edit.set(null);
|
|
500
|
+
}
|
|
501
|
+
/** Remove a validator (string or object) from the list and emit. */
|
|
502
|
+
remove(validator) {
|
|
503
|
+
const next = this.value.filter((item) => !this.#sameValidator(item, validator));
|
|
504
|
+
this.#commit(next);
|
|
505
|
+
if (this.isEditing(validator)) {
|
|
506
|
+
this.#edit.set(null);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/** Update the draft's message, or replace its raw expression as the user types. */
|
|
510
|
+
onTextChange(event, key) {
|
|
511
|
+
const value = event.target.value;
|
|
512
|
+
if (key === 'expression') {
|
|
513
|
+
this.#syncExpression(value);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this.#edit.update((draft) => {
|
|
517
|
+
const base = draft ?? this.#emptyDraft();
|
|
518
|
+
return { ...base, message: value };
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/** Toggle whether users may override (warn instead of block) this validator. */
|
|
522
|
+
setCanOverride(checked) {
|
|
523
|
+
this.#edit.update((draft) => (draft ? { ...draft, canOverride: checked } : draft));
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Persist the open draft. A new draft (no id) is appended with a fresh uuid;
|
|
527
|
+
* an existing draft (has id) replaces the matching entry. Emits the new list.
|
|
528
|
+
*/
|
|
529
|
+
saveVariable() {
|
|
530
|
+
const draft = this.#edit();
|
|
531
|
+
if (!draft || !draft.message || !draft.expression) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (draft.id !== undefined) {
|
|
535
|
+
const id = draft.id;
|
|
536
|
+
const saved = { ...draft, id };
|
|
537
|
+
this.#commit(this.value.map((item) => typeof item !== 'string' && item.id === id ? saved : item));
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
const saved = { ...draft, id: v4() };
|
|
541
|
+
this.#commit([...this.value, saved]);
|
|
542
|
+
}
|
|
543
|
+
this.#edit.set(null);
|
|
544
|
+
}
|
|
545
|
+
// ---- guided condition builder ------------------------------------------
|
|
546
|
+
/** Switch the All/Any connector joining the conditions. */
|
|
547
|
+
setConnector(connector) {
|
|
548
|
+
this.#applyGroup({ ...this.#group(), connector });
|
|
549
|
+
}
|
|
550
|
+
/** Append a fresh condition, defaulting its field to the owning input. */
|
|
551
|
+
addCondition() {
|
|
552
|
+
const fallback = this.directFields().find((f) => f.isSelf) ??
|
|
553
|
+
this.directFields()[0] ??
|
|
554
|
+
this.fieldOptions()[0];
|
|
555
|
+
const group = this.#group();
|
|
556
|
+
this.#applyGroup({
|
|
557
|
+
...group,
|
|
558
|
+
conditions: [...group.conditions, emptyCondition(fallback?.variable ?? '')],
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/** Apply a partial change to the condition at `index`. */
|
|
562
|
+
updateCondition(index, patch) {
|
|
563
|
+
const group = this.#group();
|
|
564
|
+
const conditions = group.conditions.map((c, i) => (i === index ? { ...c, ...patch } : c));
|
|
565
|
+
this.#applyGroup({ ...group, conditions });
|
|
566
|
+
}
|
|
567
|
+
/** Flip a condition between comparing to a typed value and another field. */
|
|
568
|
+
toggleCompareTo(index) {
|
|
569
|
+
const condition = this.#group().conditions[index];
|
|
570
|
+
if (!condition)
|
|
571
|
+
return;
|
|
572
|
+
if (condition.compareTo === 'value') {
|
|
573
|
+
const field = this.fieldOptions().find((f) => f.variable !== condition.field);
|
|
574
|
+
this.updateCondition(index, { compareTo: 'field', value: field?.variable ?? '' });
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
this.updateCondition(index, { compareTo: 'value', value: '' });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** Remove the condition at `index`. */
|
|
581
|
+
removeCondition(index) {
|
|
582
|
+
const group = this.#group();
|
|
583
|
+
this.#applyGroup({ ...group, conditions: group.conditions.filter((_, i) => i !== index) });
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Choose (or clear, by re-clicking) the aggregate that reduces a referenced
|
|
587
|
+
* list field to a single value.
|
|
588
|
+
*/
|
|
589
|
+
setFunction(variable, fn) {
|
|
590
|
+
this.#functions.update((current) => {
|
|
591
|
+
const next = { ...current };
|
|
592
|
+
if (next[variable] === fn) {
|
|
593
|
+
delete next[variable];
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
next[variable] = fn;
|
|
597
|
+
}
|
|
598
|
+
return next;
|
|
599
|
+
});
|
|
600
|
+
// The expression text is unchanged, but the observed deps now carry the
|
|
601
|
+
// aggregate — re-sync so inputsObservedForChanges reflects the choice.
|
|
602
|
+
this.#syncExpression(this.#edit()?.expression ?? '');
|
|
603
|
+
}
|
|
604
|
+
/** The aggregate currently chosen for a list variable, if any. */
|
|
605
|
+
currentFunction(variable) {
|
|
606
|
+
return this.#functions()[variable];
|
|
607
|
+
}
|
|
608
|
+
/** Switch to the raw expression editor. */
|
|
609
|
+
useRaw() {
|
|
610
|
+
this.#raw.set(true);
|
|
611
|
+
}
|
|
612
|
+
/** Switch back to the guided builder, reparsing the current expression. */
|
|
613
|
+
useGuided() {
|
|
614
|
+
if (!this.canUseGuided())
|
|
615
|
+
return;
|
|
616
|
+
this.#initGroupFromExpression();
|
|
617
|
+
this.#raw.set(false);
|
|
618
|
+
}
|
|
619
|
+
/** Read the value-event target as a string. */
|
|
620
|
+
inputValue(event) {
|
|
621
|
+
return event.target.value;
|
|
622
|
+
}
|
|
623
|
+
// ---- internal ----------------------------------------------------------
|
|
624
|
+
#emptyDraft() {
|
|
625
|
+
return {
|
|
626
|
+
message: '',
|
|
627
|
+
expression: '',
|
|
628
|
+
canOverride: false,
|
|
629
|
+
inputsObservedForChanges: [],
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
#cloneValidator(validator) {
|
|
633
|
+
return {
|
|
634
|
+
...validator,
|
|
635
|
+
inputsObservedForChanges: validator.inputsObservedForChanges.map((i) => ({ ...i })),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
#sameValidator(item, target) {
|
|
639
|
+
if (typeof item === 'string' || typeof target === 'string') {
|
|
640
|
+
return item === target;
|
|
641
|
+
}
|
|
642
|
+
return item.id === target.id;
|
|
643
|
+
}
|
|
644
|
+
/** Set of field variable names the builder recognises (for parsing). */
|
|
645
|
+
#fieldNameSet() {
|
|
646
|
+
return new Set(this.fieldOptions().map((f) => f.variable));
|
|
647
|
+
}
|
|
648
|
+
/** Whether an input carries a numeric data type. */
|
|
649
|
+
#isNumeric(input) {
|
|
650
|
+
return input.dataType === InputDataTypes.Number || input.type === InputTypes.Number;
|
|
651
|
+
}
|
|
652
|
+
/** Recover the per-variable aggregate choices from a draft's observed inputs. */
|
|
653
|
+
#functionsFromDraft() {
|
|
654
|
+
const map = {};
|
|
655
|
+
for (const observed of this.#edit()?.inputsObservedForChanges ?? []) {
|
|
656
|
+
if (observed.function)
|
|
657
|
+
map[observed.variable] = observed.function;
|
|
658
|
+
}
|
|
659
|
+
return map;
|
|
660
|
+
}
|
|
661
|
+
/** Seed the builder state when a draft opens: guided if parseable, else raw. */
|
|
662
|
+
#initBuilderFromDraft() {
|
|
663
|
+
this.#functions.set(this.#functionsFromDraft());
|
|
664
|
+
const parsed = parseConditions(this.#edit()?.expression ?? '', this.#fieldNameSet());
|
|
665
|
+
if (parsed) {
|
|
666
|
+
this.#group.set(parsed);
|
|
667
|
+
this.#raw.set(false);
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
this.#raw.set(true);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/** Reparse the current draft expression into the guided model. */
|
|
674
|
+
#initGroupFromExpression() {
|
|
675
|
+
const parsed = parseConditions(this.#edit()?.expression ?? '', this.#fieldNameSet());
|
|
676
|
+
if (parsed) {
|
|
677
|
+
this.#group.set(parsed);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/** Store a builder change: update the model, then serialise into the draft. */
|
|
681
|
+
#applyGroup(next) {
|
|
682
|
+
this.#group.set(next);
|
|
683
|
+
this.#syncExpression(serializeConditions(next));
|
|
684
|
+
}
|
|
685
|
+
/** Write an expression onto the draft and re-derive its observed-input deps. */
|
|
686
|
+
#syncExpression(expression) {
|
|
687
|
+
const fields = this.fieldOptions();
|
|
688
|
+
const functions = this.#functions();
|
|
689
|
+
this.#edit.update((draft) => {
|
|
690
|
+
const base = draft ?? this.#emptyDraft();
|
|
691
|
+
return {
|
|
692
|
+
...base,
|
|
693
|
+
expression,
|
|
694
|
+
inputsObservedForChanges: deriveObservedInputs(expression, fields, functions),
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/** Single mutation path: store the new list and notify the parent. */
|
|
699
|
+
#commit(next) {
|
|
700
|
+
this.value = next;
|
|
701
|
+
this.markAsTouched();
|
|
702
|
+
this.stateChanges.next();
|
|
703
|
+
this.valueChanged.emit([...next]);
|
|
704
|
+
}
|
|
705
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ValidatorsConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
706
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ValidatorsConfigComponent, isStandalone: true, selector: "lib-validators-config", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: false, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: false, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: false, isRequired: false, transformFunction: null }, mapToData: { classPropertyName: "mapToData", publicName: "mapToData", isSignal: true, isRequired: false, transformFunction: null }, formInputs: { classPropertyName: "formInputs", publicName: "formInputs", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChanged: "valueChanged" }, host: { properties: { "attr.id": "id" }, classAttribute: "lib-validators-config" }, providers: [{ provide: MatFormFieldControl, useExisting: ValidatorsConfigComponent }], ngImport: i0, template: "<section class=\"lib-validators-config__list\">\n <h5 class=\"lib-validators-config__title\">Validation rules</h5>\n\n <p class=\"lib-validators-config__hint\">\n <mat-icon class=\"lib-validators-config__hint-icon\">lightbulb</mat-icon>\n <span>Each rule shows its message when its condition is true \u2014 describe when the entry is\n <strong>wrong</strong>.</span>\n </p>\n\n <mat-chip-listbox [disabled]=\"disabled\" aria-label=\"Validation rules\">\n @for (validator of value; track trackValidator(validator)) {\n <mat-chip-option\n class=\"lib-validators-config__chip\"\n [selected]=\"isEditing(validator)\"\n (click)=\"editV(validator)\"\n >\n <span class=\"lib-validators-config__chip-label\">\n {{ validatorLabel(validator) | titlecase }}\n </span>\n\n @if (isEditing(validator)) {\n <mat-icon matChipTrailingIcon>check_circle</mat-icon>\n } @else {\n <button matChipRemove [attr.aria-label]=\"'remove validator'\" (click)=\"remove(validator)\">\n <mat-icon>cancel</mat-icon>\n </button>\n }\n </mat-chip-option>\n }\n\n @if (!edit()) {\n <mat-chip-option\n class=\"lib-validators-config__add\"\n matTooltip=\"Add a new validation rule\"\n [selectable]=\"false\"\n [disabled]=\"disabled\"\n (click)=\"addValidator()\"\n >\n Add rule\n <mat-icon matChipTrailingIcon>add</mat-icon>\n </mat-chip-option>\n }\n </mat-chip-listbox>\n</section>\n\n@if (edit(); as draft) {\n<mat-card class=\"lib-validators-config__editor\" (click)=\"$event.stopPropagation()\">\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Error message</span>\n <textarea\n class=\"lib-validators-config__control lib-validators-config__textarea\"\n rows=\"2\"\n [value]=\"draft.message\"\n (input)=\"onTextChange($event, 'message')\"\n placeholder=\"e.g. End date must be after the start date\"\n ></textarea>\n </label>\n\n <div class=\"lib-validators-config__condition-head\">\n <span class=\"lib-validators-config__f-label\">Show this error when</span>\n @if (!raw() && group().conditions.length > 1) {\n <div class=\"lib-validators-config__match\">\n <mat-button-toggle-group\n class=\"lib-validators-config__seg\"\n [value]=\"group().connector\"\n hideSingleSelectionIndicator\n (change)=\"setConnector($event.value)\"\n aria-label=\"Combine conditions\"\n >\n <mat-button-toggle value=\"&&\">All</mat-button-toggle>\n <mat-button-toggle value=\"||\">Any</mat-button-toggle>\n </mat-button-toggle-group>\n <span class=\"lib-validators-config__muted\">of these match</span>\n </div>\n }\n </div>\n\n @if (raw()) {\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Expression</span>\n <textarea\n class=\"lib-validators-config__control lib-validators-config__textarea lib-validators-config__mono\"\n rows=\"2\"\n [value]=\"draft.expression\"\n (input)=\"onTextChange($event, 'expression')\"\n placeholder='e.g. endDate < startDate'\n ></textarea>\n </label>\n @if (syntaxError(); as error) {\n <p class=\"lib-validators-config__error\">\n <mat-icon class=\"lib-validators-config__error-icon\">error_outline</mat-icon>\n <span>{{ error }}</span>\n </p>\n } @else {\n <p class=\"lib-validators-config__sub-hint\">\n True means the entry is invalid. Reference a field by its name; comparisons use\n <code>===</code>, <code>></code>, <code>includes</code> \u2026\n </p>\n }\n } @else {\n @if (fieldOptions().length === 0) {\n <p class=\"lib-validators-config__sub-hint\">\n No fields available yet \u2014 switch to Advanced to type an expression.\n </p>\n }\n @for (cond of group().conditions; track $index; let first = $first) {\n @if (!first) {\n <div class=\"lib-validators-config__connector\" aria-hidden=\"true\">\n <span>{{ group().connector === '&&' ? 'AND' : 'OR' }}</span>\n </div>\n }\n <div class=\"lib-validators-config__cond\">\n <div class=\"lib-validators-config__cond-body\">\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Field</span>\n <select\n class=\"lib-validators-config__control\"\n [class.lib-validators-config__control--invalid]=\"!cond.field\"\n (change)=\"updateCondition($index, { field: inputValue($event) })\"\n >\n <option value=\"\" disabled [selected]=\"!cond.field\">Choose a field</option>\n @if (directFields().length) {\n <optgroup label=\"Fields\">\n @for (f of directFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.field\">\n {{ f.label }}{{ f.isSelf ? ' (this field)' : '' }}\n </option>\n }\n </optgroup>\n }\n @if (listFields().length) {\n <optgroup label=\"List fields (totalled)\">\n @for (f of listFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.field\">{{ f.label }}</option>\n }\n </optgroup>\n }\n </select>\n </label>\n\n <div class=\"lib-validators-config__cond-pair\">\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Condition</span>\n <select\n class=\"lib-validators-config__control\"\n (change)=\"updateCondition($index, { operator: inputValue($event) })\"\n >\n @for (op of operators; track op.value) {\n <option [value]=\"op.value\" [selected]=\"op.value === cond.operator\">{{ op.label }}</option>\n }\n </select>\n </label>\n\n <div class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">\n {{ cond.compareTo === 'field' ? 'Other field' : 'Value' }}\n </span>\n <div class=\"lib-validators-config__value\">\n @if (cond.compareTo === 'field') {\n <select\n class=\"lib-validators-config__control\"\n [class.lib-validators-config__control--invalid]=\"!cond.value\"\n (change)=\"updateCondition($index, { value: inputValue($event) })\"\n >\n <option value=\"\" disabled [selected]=\"!cond.value\">Choose a field</option>\n @if (directFields().length) {\n <optgroup label=\"Fields\">\n @for (f of directFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.value\">\n {{ f.label }}{{ f.isSelf ? ' (this field)' : '' }}\n </option>\n }\n </optgroup>\n }\n @if (listFields().length) {\n <optgroup label=\"List fields (totalled)\">\n @for (f of listFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.value\">{{ f.label }}</option>\n }\n </optgroup>\n }\n </select>\n } @else {\n <input\n class=\"lib-validators-config__control\"\n [class.lib-validators-config__control--invalid]=\"!cond.value\"\n [value]=\"cond.value\"\n (input)=\"updateCondition($index, { value: inputValue($event) })\"\n placeholder=\"e.g. 18\"\n />\n }\n <button\n type=\"button\"\n class=\"lib-validators-config__value-toggle\"\n (click)=\"toggleCompareTo($index)\"\n [attr.aria-label]=\"\n cond.compareTo === 'field' ? 'Compare to a typed value' : 'Compare to another field'\n \"\n [matTooltip]=\"\n cond.compareTo === 'field' ? 'Compare to a typed value' : 'Compare to another field'\n \"\n >\n <mat-icon>{{ cond.compareTo === 'field' ? 'edit' : 'link' }}</mat-icon>\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <button\n type=\"button\"\n class=\"lib-validators-config__cond-remove\"\n mat-icon-button\n (click)=\"removeCondition($index)\"\n matTooltip=\"Remove condition\"\n aria-label=\"Remove condition\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n }\n\n <button type=\"button\" class=\"lib-validators-config__link\" (click)=\"addCondition()\">\n <mat-icon>add</mat-icon> Add condition\n </button>\n }\n\n @if (referencedListFields().length) {\n <div class=\"lib-validators-config__lists\">\n <span class=\"lib-validators-config__f-label\">Combine each list</span>\n <p class=\"lib-validators-config__sub-hint\">\n A list field has many rows. Choose how to reduce it to one value when comparing against a\n single field or value \u2014 optional when comparing two lists.\n </p>\n @for (f of referencedListFields(); track f.variable) {\n <div\n class=\"lib-validators-config__list-row\"\n [class.lib-validators-config__list-row--unset]=\"\n isAggregateRequired(f.variable) && !currentFunction(f.variable)\n \"\n >\n <span class=\"lib-validators-config__list-name\">\n {{ f.label }}\n @if (!isAggregateRequired(f.variable)) {\n <span class=\"lib-validators-config__list-optional\">optional</span>\n }\n </span>\n <div class=\"lib-validators-config__agg\" role=\"group\" [attr.aria-label]=\"'Combine ' + f.label\">\n @for (agg of aggregatesFor(f); track agg.value) {\n <button\n type=\"button\"\n class=\"lib-validators-config__agg-chip\"\n [class.lib-validators-config__agg-chip--on]=\"currentFunction(f.variable) === agg.value\"\n (click)=\"setFunction(f.variable, agg.value)\"\n >\n {{ agg.label }}\n </button>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @if (draft.expression) {\n <div class=\"lib-validators-config__preview\">\n <span class=\"lib-validators-config__f-label\">Generated</span>\n <code>{{ draft.expression }}</code>\n </div>\n }\n\n <div class=\"lib-validators-config__override\">\n <mat-slide-toggle\n name=\"canOverride\"\n [checked]=\"draft.canOverride\"\n (change)=\"setCanOverride($event.checked)\"\n >\n <span class=\"lib-validators-config__toggle-label\">Users can override this rule</span>\n </mat-slide-toggle>\n <span class=\"lib-validators-config__sub-hint\">\n @if (draft.canOverride) { Shown as a warning \u2014 the form can still be submitted. } @else { Blocks\n submission until the entry is fixed. }\n </span>\n </div>\n\n <div class=\"lib-validators-config__foot\">\n @if (raw()) {\n <button\n type=\"button\"\n class=\"lib-validators-config__link\"\n [disabled]=\"!canUseGuided()\"\n [matTooltip]=\"canUseGuided() ? '' : 'This expression is too advanced for the guided builder'\"\n (click)=\"useGuided()\"\n >\n <mat-icon>view_module</mat-icon> Guided\n </button>\n } @else {\n <button type=\"button\" class=\"lib-validators-config__link\" (click)=\"useRaw()\">\n <mat-icon>code</mat-icon> Advanced\n </button>\n }\n </div>\n\n @if (saveHint(); as hint) {\n <p class=\"lib-validators-config__save-hint\">\n <mat-icon class=\"lib-validators-config__save-hint-icon\">info</mat-icon>\n <span>{{ hint }}</span>\n </p>\n }\n\n <mat-toolbar class=\"lib-validators-config__toolbar\">\n <span class=\"lib-validators-config__spacer\"></span>\n <button mat-button (click)=\"closeEditor()\">Close</button>\n <button mat-raised-button color=\"accent\" [disabled]=\"!canSave()\" (click)=\"saveVariable()\">\n Save rule\n </button>\n </mat-toolbar>\n</mat-card>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.lib-validators-config__title{margin:0 0 .5rem;font-size:.9375rem;font-weight:500;letter-spacing:-.01em;color:var(--lib-forms-on-surface)}.lib-validators-config__hint{display:flex;align-items:flex-start;gap:.5rem;margin:0 0 1.25rem;font-size:.8125rem;line-height:1.5;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__hint-icon{flex:none;font-size:1.125rem;width:1.125rem;height:1.125rem;color:var(--sem-info, var(--lib-forms-primary))}.lib-validators-config__chip{height:fit-content;padding:.5rem;cursor:pointer}.lib-validators-config__chip-label{white-space:normal}.lib-validators-config__add{cursor:pointer}.lib-validators-config__editor{display:flex;flex-direction:column;gap:1.25rem;margin-top:1.25rem;padding:1.5rem;background:var(--lib-forms-surface-container-low);border-radius:var(--lib-forms-radius-lg, 16px);box-shadow:var(--mat-sys-level1, 0 1px 3px rgba(0, 0, 0, .1))}.lib-validators-config__field{display:flex;flex-direction:column;gap:.25rem;min-width:0}.lib-validators-config__f-label{font-size:.625rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__control{box-sizing:border-box;width:100%;min-width:0;height:2.125rem;padding:.375rem .5rem;font-family:inherit;font-size:.8125rem;line-height:1.25;color:var(--lib-forms-on-surface);background:var(--lib-forms-surface-container-lowest);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-sm);transition:border-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}select.lib-validators-config__control{cursor:pointer}.lib-validators-config__textarea{height:auto;min-height:2.75rem;line-height:1.45;resize:vertical}.lib-validators-config__mono{font-family:var(--lib-forms-font-mono, monospace)}.lib-validators-config__control:hover{border-color:var(--lib-forms-outline)}.lib-validators-config__control:focus{outline:none;border-color:var(--lib-forms-primary);box-shadow:0 0 0 1px var(--lib-forms-primary)}.lib-validators-config__control--invalid:not(:focus){border-color:var(--sem-warning, var(--lib-forms-outline));background:var(--sem-warning-surface, var(--lib-forms-surface-container-lowest))}.lib-validators-config__condition-head{display:flex;flex-direction:column;gap:.625rem}.lib-validators-config__match{display:flex;align-items:center;gap:.625rem;flex-wrap:wrap}.lib-validators-config__muted{font-size:.8125rem;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__seg{--mat-standard-button-toggle-height: 30px;height:30px}.lib-validators-config__connector{display:flex;justify-content:center;margin:-.375rem 0}.lib-validators-config__connector span{padding:.0625rem .5rem;font-size:.625rem;font-weight:600;letter-spacing:.1em;color:var(--lib-forms-on-surface-variant);background:var(--lib-forms-surface-container-high);border-radius:var(--lib-forms-radius-pill, 999px)}.lib-validators-config__cond{display:grid;grid-template-columns:1fr auto;align-items:start;gap:.375rem;padding:.75rem;background:var(--lib-forms-surface-container-lowest);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-md)}.lib-validators-config__cond-body{display:flex;flex-direction:column;gap:.625rem;min-width:0}.lib-validators-config__cond-pair{display:grid;grid-template-columns:8.5rem 1fr;gap:.5rem;min-width:0}.lib-validators-config__value{display:flex;align-items:center;gap:.25rem;min-width:0}.lib-validators-config__value-toggle{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.125rem;height:2.125rem;padding:0;color:var(--lib-forms-on-surface-variant);background:transparent;border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-sm);cursor:pointer;transition:border-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1)),color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}.lib-validators-config__value-toggle:hover{color:var(--lib-forms-primary);border-color:var(--lib-forms-primary)}.lib-validators-config__value-toggle mat-icon{font-size:1.0625rem;width:1.0625rem;height:1.0625rem}.lib-validators-config__cond-remove{width:2rem;height:2rem;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__link{display:inline-flex;align-items:center;align-self:flex-start;gap:.25rem;height:1.875rem;padding:0 .5rem;font:inherit;font-size:.8125rem;font-weight:500;color:var(--lib-forms-primary);background:transparent;border:0;border-radius:var(--lib-forms-radius-sm);cursor:pointer;transition:background-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}.lib-validators-config__link:hover:not(:disabled){background:var(--lib-forms-primary-container)}.lib-validators-config__link:disabled{color:var(--lib-forms-on-surface-variant);cursor:default;opacity:.6}.lib-validators-config__link mat-icon{flex:none;font-size:1.0625rem;width:1.0625rem;height:1.0625rem}.lib-validators-config__lists{display:flex;flex-direction:column;gap:.5rem;padding:.75rem;background:var(--lib-forms-surface-container-lowest);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-md)}.lib-validators-config__list-row{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap}.lib-validators-config__list-name{font-size:.8125rem;font-weight:500;color:var(--lib-forms-on-surface)}.lib-validators-config__list-row--unset .lib-validators-config__list-name:after{content:\" \\2014 pick one\";font-weight:400;color:var(--sem-warning, var(--lib-forms-on-surface-variant))}.lib-validators-config__list-optional{margin-left:.375rem;font-size:.625rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__agg{display:inline-flex;flex-wrap:wrap;gap:.25rem}.lib-validators-config__agg-chip{padding:.1875rem .5rem;font:inherit;font-size:.75rem;font-weight:500;color:var(--lib-forms-on-surface-variant);background:var(--lib-forms-surface-container);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-pill, 999px);cursor:pointer;transition:background-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1)),color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1)),border-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}.lib-validators-config__agg-chip:hover{border-color:var(--lib-forms-primary);color:var(--lib-forms-primary)}.lib-validators-config__agg-chip--on{color:var(--mat-sys-on-primary, #fff);background:var(--lib-forms-primary);border-color:var(--lib-forms-primary)}.lib-validators-config__preview{display:flex;align-items:baseline;gap:.5rem;padding:.5rem .625rem;background:var(--lib-forms-surface-container-lowest);border:1px dashed var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-sm)}.lib-validators-config__preview .lib-validators-config__f-label{flex:none}.lib-validators-config__preview code{font-family:var(--lib-forms-font-mono, monospace);font-size:.75rem;color:var(--lib-forms-on-surface-variant);word-break:break-word}.lib-validators-config__sub-hint{margin:0;font-size:.75rem;line-height:1.45;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__sub-hint code,.lib-validators-config__error code{font-family:var(--lib-forms-font-mono, monospace);padding:.0625rem .25rem;background:var(--lib-forms-surface-container-high);border-radius:var(--lib-forms-radius-sm)}.lib-validators-config__error{display:flex;align-items:flex-start;gap:.375rem;margin:0;font-size:.75rem;line-height:1.45;color:var(--sem-error, var(--mat-sys-error))}.lib-validators-config__error-icon{flex:none;font-size:1rem;width:1rem;height:1rem}.lib-validators-config__override{display:flex;flex-direction:column;gap:.5rem}.lib-validators-config__toggle-label{margin-left:.5rem;font-size:.8125rem}.lib-validators-config__foot{display:flex;justify-content:flex-end;margin-top:-.5rem}.lib-validators-config__save-hint{display:flex;align-items:flex-start;gap:.375rem;margin:0;font-size:.75rem;line-height:1.45;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__save-hint-icon{flex:none;font-size:1rem;width:1rem;height:1rem;color:var(--sem-info, var(--lib-forms-primary))}.lib-validators-config__toolbar{background:transparent;padding:0}.lib-validators-config__spacer{flex:1 1 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i1.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i1.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "directive", type: i1.MatChipRemove, selector: "[matChipRemove]" }, { kind: "directive", type: i1.MatChipTrailingIcon, selector: "mat-chip-trailing-icon, [matChipTrailingIcon]" }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i5.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i1$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i2.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i2.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i1$2.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i3$2.TitleCasePipe, name: "titlecase" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
707
|
+
}
|
|
708
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ValidatorsConfigComponent, decorators: [{
|
|
709
|
+
type: Component,
|
|
710
|
+
args: [{ selector: 'lib-validators-config', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [
|
|
711
|
+
CommonModule,
|
|
712
|
+
MatIconModule,
|
|
713
|
+
MatChipsModule,
|
|
714
|
+
MatCardModule,
|
|
715
|
+
MatToolbarModule,
|
|
716
|
+
MatButtonModule,
|
|
717
|
+
MatButtonToggleModule,
|
|
718
|
+
MatSlideToggleModule,
|
|
719
|
+
MatTooltipModule,
|
|
720
|
+
], providers: [{ provide: MatFormFieldControl, useExisting: ValidatorsConfigComponent }], host: {
|
|
721
|
+
'class': 'lib-validators-config',
|
|
722
|
+
'[attr.id]': 'id',
|
|
723
|
+
}, template: "<section class=\"lib-validators-config__list\">\n <h5 class=\"lib-validators-config__title\">Validation rules</h5>\n\n <p class=\"lib-validators-config__hint\">\n <mat-icon class=\"lib-validators-config__hint-icon\">lightbulb</mat-icon>\n <span>Each rule shows its message when its condition is true \u2014 describe when the entry is\n <strong>wrong</strong>.</span>\n </p>\n\n <mat-chip-listbox [disabled]=\"disabled\" aria-label=\"Validation rules\">\n @for (validator of value; track trackValidator(validator)) {\n <mat-chip-option\n class=\"lib-validators-config__chip\"\n [selected]=\"isEditing(validator)\"\n (click)=\"editV(validator)\"\n >\n <span class=\"lib-validators-config__chip-label\">\n {{ validatorLabel(validator) | titlecase }}\n </span>\n\n @if (isEditing(validator)) {\n <mat-icon matChipTrailingIcon>check_circle</mat-icon>\n } @else {\n <button matChipRemove [attr.aria-label]=\"'remove validator'\" (click)=\"remove(validator)\">\n <mat-icon>cancel</mat-icon>\n </button>\n }\n </mat-chip-option>\n }\n\n @if (!edit()) {\n <mat-chip-option\n class=\"lib-validators-config__add\"\n matTooltip=\"Add a new validation rule\"\n [selectable]=\"false\"\n [disabled]=\"disabled\"\n (click)=\"addValidator()\"\n >\n Add rule\n <mat-icon matChipTrailingIcon>add</mat-icon>\n </mat-chip-option>\n }\n </mat-chip-listbox>\n</section>\n\n@if (edit(); as draft) {\n<mat-card class=\"lib-validators-config__editor\" (click)=\"$event.stopPropagation()\">\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Error message</span>\n <textarea\n class=\"lib-validators-config__control lib-validators-config__textarea\"\n rows=\"2\"\n [value]=\"draft.message\"\n (input)=\"onTextChange($event, 'message')\"\n placeholder=\"e.g. End date must be after the start date\"\n ></textarea>\n </label>\n\n <div class=\"lib-validators-config__condition-head\">\n <span class=\"lib-validators-config__f-label\">Show this error when</span>\n @if (!raw() && group().conditions.length > 1) {\n <div class=\"lib-validators-config__match\">\n <mat-button-toggle-group\n class=\"lib-validators-config__seg\"\n [value]=\"group().connector\"\n hideSingleSelectionIndicator\n (change)=\"setConnector($event.value)\"\n aria-label=\"Combine conditions\"\n >\n <mat-button-toggle value=\"&&\">All</mat-button-toggle>\n <mat-button-toggle value=\"||\">Any</mat-button-toggle>\n </mat-button-toggle-group>\n <span class=\"lib-validators-config__muted\">of these match</span>\n </div>\n }\n </div>\n\n @if (raw()) {\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Expression</span>\n <textarea\n class=\"lib-validators-config__control lib-validators-config__textarea lib-validators-config__mono\"\n rows=\"2\"\n [value]=\"draft.expression\"\n (input)=\"onTextChange($event, 'expression')\"\n placeholder='e.g. endDate < startDate'\n ></textarea>\n </label>\n @if (syntaxError(); as error) {\n <p class=\"lib-validators-config__error\">\n <mat-icon class=\"lib-validators-config__error-icon\">error_outline</mat-icon>\n <span>{{ error }}</span>\n </p>\n } @else {\n <p class=\"lib-validators-config__sub-hint\">\n True means the entry is invalid. Reference a field by its name; comparisons use\n <code>===</code>, <code>></code>, <code>includes</code> \u2026\n </p>\n }\n } @else {\n @if (fieldOptions().length === 0) {\n <p class=\"lib-validators-config__sub-hint\">\n No fields available yet \u2014 switch to Advanced to type an expression.\n </p>\n }\n @for (cond of group().conditions; track $index; let first = $first) {\n @if (!first) {\n <div class=\"lib-validators-config__connector\" aria-hidden=\"true\">\n <span>{{ group().connector === '&&' ? 'AND' : 'OR' }}</span>\n </div>\n }\n <div class=\"lib-validators-config__cond\">\n <div class=\"lib-validators-config__cond-body\">\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Field</span>\n <select\n class=\"lib-validators-config__control\"\n [class.lib-validators-config__control--invalid]=\"!cond.field\"\n (change)=\"updateCondition($index, { field: inputValue($event) })\"\n >\n <option value=\"\" disabled [selected]=\"!cond.field\">Choose a field</option>\n @if (directFields().length) {\n <optgroup label=\"Fields\">\n @for (f of directFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.field\">\n {{ f.label }}{{ f.isSelf ? ' (this field)' : '' }}\n </option>\n }\n </optgroup>\n }\n @if (listFields().length) {\n <optgroup label=\"List fields (totalled)\">\n @for (f of listFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.field\">{{ f.label }}</option>\n }\n </optgroup>\n }\n </select>\n </label>\n\n <div class=\"lib-validators-config__cond-pair\">\n <label class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">Condition</span>\n <select\n class=\"lib-validators-config__control\"\n (change)=\"updateCondition($index, { operator: inputValue($event) })\"\n >\n @for (op of operators; track op.value) {\n <option [value]=\"op.value\" [selected]=\"op.value === cond.operator\">{{ op.label }}</option>\n }\n </select>\n </label>\n\n <div class=\"lib-validators-config__field\">\n <span class=\"lib-validators-config__f-label\">\n {{ cond.compareTo === 'field' ? 'Other field' : 'Value' }}\n </span>\n <div class=\"lib-validators-config__value\">\n @if (cond.compareTo === 'field') {\n <select\n class=\"lib-validators-config__control\"\n [class.lib-validators-config__control--invalid]=\"!cond.value\"\n (change)=\"updateCondition($index, { value: inputValue($event) })\"\n >\n <option value=\"\" disabled [selected]=\"!cond.value\">Choose a field</option>\n @if (directFields().length) {\n <optgroup label=\"Fields\">\n @for (f of directFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.value\">\n {{ f.label }}{{ f.isSelf ? ' (this field)' : '' }}\n </option>\n }\n </optgroup>\n }\n @if (listFields().length) {\n <optgroup label=\"List fields (totalled)\">\n @for (f of listFields(); track f.variable) {\n <option [value]=\"f.variable\" [selected]=\"f.variable === cond.value\">{{ f.label }}</option>\n }\n </optgroup>\n }\n </select>\n } @else {\n <input\n class=\"lib-validators-config__control\"\n [class.lib-validators-config__control--invalid]=\"!cond.value\"\n [value]=\"cond.value\"\n (input)=\"updateCondition($index, { value: inputValue($event) })\"\n placeholder=\"e.g. 18\"\n />\n }\n <button\n type=\"button\"\n class=\"lib-validators-config__value-toggle\"\n (click)=\"toggleCompareTo($index)\"\n [attr.aria-label]=\"\n cond.compareTo === 'field' ? 'Compare to a typed value' : 'Compare to another field'\n \"\n [matTooltip]=\"\n cond.compareTo === 'field' ? 'Compare to a typed value' : 'Compare to another field'\n \"\n >\n <mat-icon>{{ cond.compareTo === 'field' ? 'edit' : 'link' }}</mat-icon>\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <button\n type=\"button\"\n class=\"lib-validators-config__cond-remove\"\n mat-icon-button\n (click)=\"removeCondition($index)\"\n matTooltip=\"Remove condition\"\n aria-label=\"Remove condition\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n }\n\n <button type=\"button\" class=\"lib-validators-config__link\" (click)=\"addCondition()\">\n <mat-icon>add</mat-icon> Add condition\n </button>\n }\n\n @if (referencedListFields().length) {\n <div class=\"lib-validators-config__lists\">\n <span class=\"lib-validators-config__f-label\">Combine each list</span>\n <p class=\"lib-validators-config__sub-hint\">\n A list field has many rows. Choose how to reduce it to one value when comparing against a\n single field or value \u2014 optional when comparing two lists.\n </p>\n @for (f of referencedListFields(); track f.variable) {\n <div\n class=\"lib-validators-config__list-row\"\n [class.lib-validators-config__list-row--unset]=\"\n isAggregateRequired(f.variable) && !currentFunction(f.variable)\n \"\n >\n <span class=\"lib-validators-config__list-name\">\n {{ f.label }}\n @if (!isAggregateRequired(f.variable)) {\n <span class=\"lib-validators-config__list-optional\">optional</span>\n }\n </span>\n <div class=\"lib-validators-config__agg\" role=\"group\" [attr.aria-label]=\"'Combine ' + f.label\">\n @for (agg of aggregatesFor(f); track agg.value) {\n <button\n type=\"button\"\n class=\"lib-validators-config__agg-chip\"\n [class.lib-validators-config__agg-chip--on]=\"currentFunction(f.variable) === agg.value\"\n (click)=\"setFunction(f.variable, agg.value)\"\n >\n {{ agg.label }}\n </button>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @if (draft.expression) {\n <div class=\"lib-validators-config__preview\">\n <span class=\"lib-validators-config__f-label\">Generated</span>\n <code>{{ draft.expression }}</code>\n </div>\n }\n\n <div class=\"lib-validators-config__override\">\n <mat-slide-toggle\n name=\"canOverride\"\n [checked]=\"draft.canOverride\"\n (change)=\"setCanOverride($event.checked)\"\n >\n <span class=\"lib-validators-config__toggle-label\">Users can override this rule</span>\n </mat-slide-toggle>\n <span class=\"lib-validators-config__sub-hint\">\n @if (draft.canOverride) { Shown as a warning \u2014 the form can still be submitted. } @else { Blocks\n submission until the entry is fixed. }\n </span>\n </div>\n\n <div class=\"lib-validators-config__foot\">\n @if (raw()) {\n <button\n type=\"button\"\n class=\"lib-validators-config__link\"\n [disabled]=\"!canUseGuided()\"\n [matTooltip]=\"canUseGuided() ? '' : 'This expression is too advanced for the guided builder'\"\n (click)=\"useGuided()\"\n >\n <mat-icon>view_module</mat-icon> Guided\n </button>\n } @else {\n <button type=\"button\" class=\"lib-validators-config__link\" (click)=\"useRaw()\">\n <mat-icon>code</mat-icon> Advanced\n </button>\n }\n </div>\n\n @if (saveHint(); as hint) {\n <p class=\"lib-validators-config__save-hint\">\n <mat-icon class=\"lib-validators-config__save-hint-icon\">info</mat-icon>\n <span>{{ hint }}</span>\n </p>\n }\n\n <mat-toolbar class=\"lib-validators-config__toolbar\">\n <span class=\"lib-validators-config__spacer\"></span>\n <button mat-button (click)=\"closeEditor()\">Close</button>\n <button mat-raised-button color=\"accent\" [disabled]=\"!canSave()\" (click)=\"saveVariable()\">\n Save rule\n </button>\n </mat-toolbar>\n</mat-card>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.lib-validators-config__title{margin:0 0 .5rem;font-size:.9375rem;font-weight:500;letter-spacing:-.01em;color:var(--lib-forms-on-surface)}.lib-validators-config__hint{display:flex;align-items:flex-start;gap:.5rem;margin:0 0 1.25rem;font-size:.8125rem;line-height:1.5;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__hint-icon{flex:none;font-size:1.125rem;width:1.125rem;height:1.125rem;color:var(--sem-info, var(--lib-forms-primary))}.lib-validators-config__chip{height:fit-content;padding:.5rem;cursor:pointer}.lib-validators-config__chip-label{white-space:normal}.lib-validators-config__add{cursor:pointer}.lib-validators-config__editor{display:flex;flex-direction:column;gap:1.25rem;margin-top:1.25rem;padding:1.5rem;background:var(--lib-forms-surface-container-low);border-radius:var(--lib-forms-radius-lg, 16px);box-shadow:var(--mat-sys-level1, 0 1px 3px rgba(0, 0, 0, .1))}.lib-validators-config__field{display:flex;flex-direction:column;gap:.25rem;min-width:0}.lib-validators-config__f-label{font-size:.625rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__control{box-sizing:border-box;width:100%;min-width:0;height:2.125rem;padding:.375rem .5rem;font-family:inherit;font-size:.8125rem;line-height:1.25;color:var(--lib-forms-on-surface);background:var(--lib-forms-surface-container-lowest);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-sm);transition:border-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}select.lib-validators-config__control{cursor:pointer}.lib-validators-config__textarea{height:auto;min-height:2.75rem;line-height:1.45;resize:vertical}.lib-validators-config__mono{font-family:var(--lib-forms-font-mono, monospace)}.lib-validators-config__control:hover{border-color:var(--lib-forms-outline)}.lib-validators-config__control:focus{outline:none;border-color:var(--lib-forms-primary);box-shadow:0 0 0 1px var(--lib-forms-primary)}.lib-validators-config__control--invalid:not(:focus){border-color:var(--sem-warning, var(--lib-forms-outline));background:var(--sem-warning-surface, var(--lib-forms-surface-container-lowest))}.lib-validators-config__condition-head{display:flex;flex-direction:column;gap:.625rem}.lib-validators-config__match{display:flex;align-items:center;gap:.625rem;flex-wrap:wrap}.lib-validators-config__muted{font-size:.8125rem;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__seg{--mat-standard-button-toggle-height: 30px;height:30px}.lib-validators-config__connector{display:flex;justify-content:center;margin:-.375rem 0}.lib-validators-config__connector span{padding:.0625rem .5rem;font-size:.625rem;font-weight:600;letter-spacing:.1em;color:var(--lib-forms-on-surface-variant);background:var(--lib-forms-surface-container-high);border-radius:var(--lib-forms-radius-pill, 999px)}.lib-validators-config__cond{display:grid;grid-template-columns:1fr auto;align-items:start;gap:.375rem;padding:.75rem;background:var(--lib-forms-surface-container-lowest);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-md)}.lib-validators-config__cond-body{display:flex;flex-direction:column;gap:.625rem;min-width:0}.lib-validators-config__cond-pair{display:grid;grid-template-columns:8.5rem 1fr;gap:.5rem;min-width:0}.lib-validators-config__value{display:flex;align-items:center;gap:.25rem;min-width:0}.lib-validators-config__value-toggle{display:inline-flex;align-items:center;justify-content:center;flex:none;width:2.125rem;height:2.125rem;padding:0;color:var(--lib-forms-on-surface-variant);background:transparent;border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-sm);cursor:pointer;transition:border-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1)),color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}.lib-validators-config__value-toggle:hover{color:var(--lib-forms-primary);border-color:var(--lib-forms-primary)}.lib-validators-config__value-toggle mat-icon{font-size:1.0625rem;width:1.0625rem;height:1.0625rem}.lib-validators-config__cond-remove{width:2rem;height:2rem;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__link{display:inline-flex;align-items:center;align-self:flex-start;gap:.25rem;height:1.875rem;padding:0 .5rem;font:inherit;font-size:.8125rem;font-weight:500;color:var(--lib-forms-primary);background:transparent;border:0;border-radius:var(--lib-forms-radius-sm);cursor:pointer;transition:background-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}.lib-validators-config__link:hover:not(:disabled){background:var(--lib-forms-primary-container)}.lib-validators-config__link:disabled{color:var(--lib-forms-on-surface-variant);cursor:default;opacity:.6}.lib-validators-config__link mat-icon{flex:none;font-size:1.0625rem;width:1.0625rem;height:1.0625rem}.lib-validators-config__lists{display:flex;flex-direction:column;gap:.5rem;padding:.75rem;background:var(--lib-forms-surface-container-lowest);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-md)}.lib-validators-config__list-row{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap}.lib-validators-config__list-name{font-size:.8125rem;font-weight:500;color:var(--lib-forms-on-surface)}.lib-validators-config__list-row--unset .lib-validators-config__list-name:after{content:\" \\2014 pick one\";font-weight:400;color:var(--sem-warning, var(--lib-forms-on-surface-variant))}.lib-validators-config__list-optional{margin-left:.375rem;font-size:.625rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__agg{display:inline-flex;flex-wrap:wrap;gap:.25rem}.lib-validators-config__agg-chip{padding:.1875rem .5rem;font:inherit;font-size:.75rem;font-weight:500;color:var(--lib-forms-on-surface-variant);background:var(--lib-forms-surface-container);border:1px solid var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-pill, 999px);cursor:pointer;transition:background-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1)),color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1)),border-color var(--lib-forms-duration-hover, .3s) var(--lib-forms-easing, cubic-bezier(.16, 1, .3, 1))}.lib-validators-config__agg-chip:hover{border-color:var(--lib-forms-primary);color:var(--lib-forms-primary)}.lib-validators-config__agg-chip--on{color:var(--mat-sys-on-primary, #fff);background:var(--lib-forms-primary);border-color:var(--lib-forms-primary)}.lib-validators-config__preview{display:flex;align-items:baseline;gap:.5rem;padding:.5rem .625rem;background:var(--lib-forms-surface-container-lowest);border:1px dashed var(--lib-forms-outline-variant);border-radius:var(--lib-forms-radius-sm)}.lib-validators-config__preview .lib-validators-config__f-label{flex:none}.lib-validators-config__preview code{font-family:var(--lib-forms-font-mono, monospace);font-size:.75rem;color:var(--lib-forms-on-surface-variant);word-break:break-word}.lib-validators-config__sub-hint{margin:0;font-size:.75rem;line-height:1.45;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__sub-hint code,.lib-validators-config__error code{font-family:var(--lib-forms-font-mono, monospace);padding:.0625rem .25rem;background:var(--lib-forms-surface-container-high);border-radius:var(--lib-forms-radius-sm)}.lib-validators-config__error{display:flex;align-items:flex-start;gap:.375rem;margin:0;font-size:.75rem;line-height:1.45;color:var(--sem-error, var(--mat-sys-error))}.lib-validators-config__error-icon{flex:none;font-size:1rem;width:1rem;height:1rem}.lib-validators-config__override{display:flex;flex-direction:column;gap:.5rem}.lib-validators-config__toggle-label{margin-left:.5rem;font-size:.8125rem}.lib-validators-config__foot{display:flex;justify-content:flex-end;margin-top:-.5rem}.lib-validators-config__save-hint{display:flex;align-items:flex-start;gap:.375rem;margin:0;font-size:.75rem;line-height:1.45;color:var(--lib-forms-on-surface-variant)}.lib-validators-config__save-hint-icon{flex:none;font-size:1rem;width:1rem;height:1rem;color:var(--sem-info, var(--lib-forms-primary))}.lib-validators-config__toolbar{background:transparent;padding:0}.lib-validators-config__spacer{flex:1 1 auto}\n"] }]
|
|
724
|
+
}], propDecorators: { disabled: [{
|
|
725
|
+
type: Input
|
|
726
|
+
}], value: [{
|
|
727
|
+
type: Input
|
|
728
|
+
}], errors: [{
|
|
729
|
+
type: Input
|
|
730
|
+
}], mapToData: [{ type: i0.Input, args: [{ isSignal: true, alias: "mapToData", required: false }] }], formInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "formInputs", required: false }] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }] } });
|
|
731
|
+
|
|
732
|
+
export { ValidatorsConfigComponent };
|
|
733
|
+
//# sourceMappingURL=ngx-t-forms-validators-config.component-oGjQVGE2.mjs.map
|