ngx-semantic 2.0.7-beta.0 → 2.0.7-beta.2
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-semantic-collections-breadcrumb.mjs +13 -10
- package/fesm2022/ngx-semantic-collections-breadcrumb.mjs.map +1 -1
- package/fesm2022/ngx-semantic-collections-form.mjs +987 -16
- package/fesm2022/ngx-semantic-collections-form.mjs.map +1 -1
- package/fesm2022/ngx-semantic-collections-grid.mjs +13 -10
- package/fesm2022/ngx-semantic-collections-grid.mjs.map +1 -1
- package/fesm2022/ngx-semantic-collections-menu.mjs +25 -13
- package/fesm2022/ngx-semantic-collections-menu.mjs.map +1 -1
- package/fesm2022/ngx-semantic-collections-message.mjs +20 -20
- package/fesm2022/ngx-semantic-collections-message.mjs.map +1 -1
- package/fesm2022/ngx-semantic-collections-table.mjs +17 -13
- package/fesm2022/ngx-semantic-collections-table.mjs.map +1 -1
- package/fesm2022/ngx-semantic-core-base.mjs +1 -0
- package/fesm2022/ngx-semantic-core-base.mjs.map +1 -1
- package/fesm2022/ngx-semantic-core-util.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-button.mjs +9 -7
- package/fesm2022/ngx-semantic-elements-button.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-container.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-divider.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-flag.mjs +5 -4
- package/fesm2022/ngx-semantic-elements-flag.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-header.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-icon.mjs +5 -4
- package/fesm2022/ngx-semantic-elements-icon.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-image.mjs +5 -4
- package/fesm2022/ngx-semantic-elements-image.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-input.mjs +5 -4
- package/fesm2022/ngx-semantic-elements-input.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-label.mjs +5 -4
- package/fesm2022/ngx-semantic-elements-label.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-list.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-loader.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-placeholder.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-rail.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-reveal.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-segment.mjs +5 -4
- package/fesm2022/ngx-semantic-elements-segment.mjs.map +1 -1
- package/fesm2022/ngx-semantic-elements-steps.mjs +5 -4
- package/fesm2022/ngx-semantic-elements-steps.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-accordion.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-checkbox.mjs +19 -19
- package/fesm2022/ngx-semantic-modules-checkbox.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-dimmer.mjs +42 -33
- package/fesm2022/ngx-semantic-modules-dimmer.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-dropdown.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-embed.mjs +22 -22
- package/fesm2022/ngx-semantic-modules-embed.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-modal.mjs +31 -32
- package/fesm2022/ngx-semantic-modules-modal.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-popup.mjs +22 -23
- package/fesm2022/ngx-semantic-modules-popup.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-progress.mjs +6 -6
- package/fesm2022/ngx-semantic-modules-progress.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-rating.mjs +15 -17
- package/fesm2022/ngx-semantic-modules-rating.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-search.mjs +34 -34
- package/fesm2022/ngx-semantic-modules-search.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-select.mjs +87 -88
- package/fesm2022/ngx-semantic-modules-select.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-shape.mjs +6 -6
- package/fesm2022/ngx-semantic-modules-shape.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-sidebar.mjs +13 -12
- package/fesm2022/ngx-semantic-modules-sidebar.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-sticky.mjs +9 -12
- package/fesm2022/ngx-semantic-modules-sticky.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-tabs.mjs +30 -30
- package/fesm2022/ngx-semantic-modules-tabs.mjs.map +1 -1
- package/fesm2022/ngx-semantic-modules-transition.mjs +721 -0
- package/fesm2022/ngx-semantic-modules-transition.mjs.map +1 -0
- package/fesm2022/ngx-semantic-views-advertisement.mjs +5 -4
- package/fesm2022/ngx-semantic-views-advertisement.mjs.map +1 -1
- package/fesm2022/ngx-semantic-views-card.mjs +6 -9
- package/fesm2022/ngx-semantic-views-card.mjs.map +1 -1
- package/fesm2022/ngx-semantic-views-comment.mjs.map +1 -1
- package/fesm2022/ngx-semantic-views-feed.mjs +26 -26
- package/fesm2022/ngx-semantic-views-feed.mjs.map +1 -1
- package/fesm2022/ngx-semantic-views-items.mjs +13 -10
- package/fesm2022/ngx-semantic-views-items.mjs.map +1 -1
- package/fesm2022/ngx-semantic-views-statistics.mjs +16 -16
- package/fesm2022/ngx-semantic-views-statistics.mjs.map +1 -1
- package/fesm2022/ngx-semantic.mjs +11 -5
- package/fesm2022/ngx-semantic.mjs.map +1 -1
- package/modules/transition/package.json +4 -0
- package/package.json +5 -1
- package/types/ngx-semantic-collections-breadcrumb.d.ts +4 -17
- package/types/ngx-semantic-collections-form.d.ts +150 -20
- package/types/ngx-semantic-collections-grid.d.ts +4 -17
- package/types/ngx-semantic-collections-menu.d.ts +4 -17
- package/types/ngx-semantic-collections-message.d.ts +1 -3
- package/types/ngx-semantic-collections-table.d.ts +5 -22
- package/types/ngx-semantic-core-base.d.ts +1 -1
- package/types/ngx-semantic-elements-button.d.ts +3 -8
- package/types/ngx-semantic-elements-flag.d.ts +2 -3
- package/types/ngx-semantic-elements-icon.d.ts +1 -2
- package/types/ngx-semantic-elements-image.d.ts +2 -7
- package/types/ngx-semantic-elements-input.d.ts +2 -7
- package/types/ngx-semantic-elements-label.d.ts +2 -7
- package/types/ngx-semantic-elements-segment.d.ts +1 -6
- package/types/ngx-semantic-elements-steps.d.ts +1 -6
- package/types/ngx-semantic-modules-checkbox.d.ts +3 -4
- package/types/ngx-semantic-modules-dimmer.d.ts +2 -3
- package/types/ngx-semantic-modules-embed.d.ts +2 -5
- package/types/ngx-semantic-modules-modal.d.ts +2 -2
- package/types/ngx-semantic-modules-popup.d.ts +2 -4
- package/types/ngx-semantic-modules-rating.d.ts +4 -4
- package/types/ngx-semantic-modules-select.d.ts +1 -2
- package/types/ngx-semantic-modules-shape.d.ts +1 -2
- package/types/ngx-semantic-modules-sidebar.d.ts +1 -15
- package/types/ngx-semantic-modules-sticky.d.ts +1 -2
- package/types/ngx-semantic-modules-transition.d.ts +107 -0
- package/types/ngx-semantic-views-advertisement.d.ts +2 -7
- package/types/ngx-semantic-views-card.d.ts +2 -2
- package/types/ngx-semantic-views-feed.d.ts +2 -11
- package/types/ngx-semantic-views-items.d.ts +4 -9
- package/types/ngx-semantic-views-statistics.d.ts +13 -26
- package/types/ngx-semantic.d.ts +2 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { __decorate } from 'tslib';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { Input, Directive, NgModule } from '@angular/core';
|
|
3
|
+
import { inject, ElementRef, Input, Directive, Renderer2, NgZone, EventEmitter, HostListener, Output, NgModule } from '@angular/core';
|
|
4
4
|
import { ClassUtils, InputBoolean } from 'ngx-semantic/core/util';
|
|
5
5
|
import { BaseDirective } from 'ngx-semantic/core/base';
|
|
6
6
|
import { CommonModule } from '@angular/common';
|
|
7
|
+
import { NgForm, FormGroupDirective, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
8
|
+
import { Subject, fromEvent, merge } from 'rxjs';
|
|
9
|
+
import { debounceTime, takeUntil } from 'rxjs/operators';
|
|
7
10
|
|
|
8
11
|
class SuiFormFieldDirective extends BaseDirective {
|
|
9
|
-
constructor(
|
|
12
|
+
constructor() {
|
|
13
|
+
const element = inject(ElementRef);
|
|
10
14
|
super(element);
|
|
11
15
|
this.suiWidth = null;
|
|
12
16
|
this.suiError = false;
|
|
@@ -25,7 +29,7 @@ class SuiFormFieldDirective extends BaseDirective {
|
|
|
25
29
|
ClassUtils.getPropClass(this.suiError, 'error')
|
|
26
30
|
].join(' ');
|
|
27
31
|
}
|
|
28
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormFieldDirective, deps: [
|
|
32
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormFieldDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
29
33
|
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: SuiFormFieldDirective, isStandalone: true, selector: "[suiFormField]", inputs: { suiWidth: "suiWidth", suiError: "suiError", suiInline: "suiInline", disabled: "disabled", suiRequired: "suiRequired" }, exportAs: ["suiFormField"], usesInheritance: true, ngImport: i0 }); }
|
|
30
34
|
}
|
|
31
35
|
__decorate([
|
|
@@ -47,7 +51,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
47
51
|
exportAs: 'suiFormField',
|
|
48
52
|
selector: '[suiFormField]'
|
|
49
53
|
}]
|
|
50
|
-
}], ctorParameters: () => [
|
|
54
|
+
}], ctorParameters: () => [], propDecorators: { suiWidth: [{
|
|
51
55
|
type: Input
|
|
52
56
|
}], suiError: [{
|
|
53
57
|
type: Input
|
|
@@ -60,7 +64,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
60
64
|
}] } });
|
|
61
65
|
|
|
62
66
|
class SuiFormFieldsDirective extends BaseDirective {
|
|
63
|
-
constructor(
|
|
67
|
+
constructor() {
|
|
68
|
+
const element = inject(ElementRef);
|
|
64
69
|
super(element);
|
|
65
70
|
this.suiWidth = null;
|
|
66
71
|
this.suiInline = false;
|
|
@@ -76,7 +81,7 @@ class SuiFormFieldsDirective extends BaseDirective {
|
|
|
76
81
|
'fields'
|
|
77
82
|
].join(' ');
|
|
78
83
|
}
|
|
79
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormFieldsDirective, deps: [
|
|
84
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormFieldsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
80
85
|
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: SuiFormFieldsDirective, isStandalone: true, selector: "[suiFormFields]", inputs: { suiWidth: "suiWidth", suiInline: "suiInline", suiGrouped: "suiGrouped", suiEqualWidth: "suiEqualWidth" }, exportAs: ["suiFormFields"], usesInheritance: true, ngImport: i0 }); }
|
|
81
86
|
}
|
|
82
87
|
__decorate([
|
|
@@ -95,7 +100,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
95
100
|
exportAs: 'suiFormFields',
|
|
96
101
|
selector: '[suiFormFields]'
|
|
97
102
|
}]
|
|
98
|
-
}], ctorParameters: () => [
|
|
103
|
+
}], ctorParameters: () => [], propDecorators: { suiWidth: [{
|
|
99
104
|
type: Input
|
|
100
105
|
}], suiInline: [{
|
|
101
106
|
type: Input
|
|
@@ -106,7 +111,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
106
111
|
}] } });
|
|
107
112
|
|
|
108
113
|
class SuiFormDirective extends BaseDirective {
|
|
109
|
-
constructor(
|
|
114
|
+
constructor() {
|
|
115
|
+
const element = inject(ElementRef);
|
|
110
116
|
super(element);
|
|
111
117
|
this.suiState = null;
|
|
112
118
|
this.suiSize = null;
|
|
@@ -127,7 +133,7 @@ class SuiFormDirective extends BaseDirective {
|
|
|
127
133
|
this.suiState
|
|
128
134
|
].join(' ');
|
|
129
135
|
}
|
|
130
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormDirective, deps: [
|
|
136
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
131
137
|
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: SuiFormDirective, isStandalone: true, selector: "[sui-form]", inputs: { suiState: "suiState", suiSize: "suiSize", suiLoading: "suiLoading", suiEqualWidth: "suiEqualWidth", suiInverted: "suiInverted", suiReply: "suiReply" }, exportAs: ["suiForm"], usesInheritance: true, ngImport: i0 }); }
|
|
132
138
|
}
|
|
133
139
|
__decorate([
|
|
@@ -149,7 +155,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
149
155
|
selector: '[sui-form]',
|
|
150
156
|
exportAs: 'suiForm'
|
|
151
157
|
}]
|
|
152
|
-
}], ctorParameters: () => [
|
|
158
|
+
}], ctorParameters: () => [], propDecorators: { suiState: [{
|
|
153
159
|
type: Input
|
|
154
160
|
}], suiSize: [{
|
|
155
161
|
type: Input
|
|
@@ -163,32 +169,997 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
163
169
|
type: Input
|
|
164
170
|
}] } });
|
|
165
171
|
|
|
172
|
+
/** Resolve control like Semantic UI: id, then name, then data-validate. */
|
|
173
|
+
function resolveFieldElement(root, identifier) {
|
|
174
|
+
if (!identifier) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const byId = root.querySelector(`#${escapeCssId(identifier)}`);
|
|
178
|
+
if (byId instanceof HTMLElement) {
|
|
179
|
+
return byId;
|
|
180
|
+
}
|
|
181
|
+
const byName = root.querySelector(`[name="${escapeAttrSelector(identifier)}"]`);
|
|
182
|
+
if (byName instanceof HTMLElement) {
|
|
183
|
+
return byName;
|
|
184
|
+
}
|
|
185
|
+
const byMeta = root.querySelector(`[data-validate="${escapeAttrSelector(identifier)}"]`);
|
|
186
|
+
return byMeta instanceof HTMLElement ? byMeta : null;
|
|
187
|
+
}
|
|
188
|
+
function escapeCssId(id) {
|
|
189
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
190
|
+
return CSS.escape(id);
|
|
191
|
+
}
|
|
192
|
+
return id.replace(/([^a-zA-Z0-9_-])/g, '\\$1');
|
|
193
|
+
}
|
|
194
|
+
function escapeAttrSelector(s) {
|
|
195
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
196
|
+
}
|
|
197
|
+
function readControlValue(el) {
|
|
198
|
+
if (el instanceof HTMLInputElement) {
|
|
199
|
+
if (el.type === 'checkbox') {
|
|
200
|
+
return el.checked;
|
|
201
|
+
}
|
|
202
|
+
if (el.type === 'radio') {
|
|
203
|
+
const form = el.form ?? el.closest('form');
|
|
204
|
+
if (form) {
|
|
205
|
+
const checked = form.querySelector(`input[type="radio"][name="${escapeAttrSelector(el.name)}"]:checked`);
|
|
206
|
+
return checked ? checked.value : '';
|
|
207
|
+
}
|
|
208
|
+
return el.checked ? el.value : '';
|
|
209
|
+
}
|
|
210
|
+
return el.value;
|
|
211
|
+
}
|
|
212
|
+
if (el instanceof HTMLSelectElement) {
|
|
213
|
+
if (el.multiple) {
|
|
214
|
+
return Array.from(el.selectedOptions).map(o => o.value);
|
|
215
|
+
}
|
|
216
|
+
return el.value;
|
|
217
|
+
}
|
|
218
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
219
|
+
return el.value;
|
|
220
|
+
}
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
function findSemanticField(el) {
|
|
224
|
+
return el.closest('.field');
|
|
225
|
+
}
|
|
226
|
+
/** Label text for prompts: label[for=id], aria-label, placeholder. */
|
|
227
|
+
function inferFieldName(el, identifier) {
|
|
228
|
+
if (el.id) {
|
|
229
|
+
const label = el.ownerDocument?.querySelector(`label[for="${escapeAttrSelector(el.id)}"]`);
|
|
230
|
+
if (label?.textContent?.trim()) {
|
|
231
|
+
return label.textContent.trim();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const aria = el.getAttribute('aria-label');
|
|
235
|
+
if (aria) {
|
|
236
|
+
return aria;
|
|
237
|
+
}
|
|
238
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
239
|
+
const ph = el.getAttribute('placeholder');
|
|
240
|
+
if (ph) {
|
|
241
|
+
return ph;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return identifier;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toRuleSpecs(rules) {
|
|
248
|
+
if (typeof rules === 'string') {
|
|
249
|
+
return [{ type: rules }];
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(rules)) {
|
|
252
|
+
return rules.map(r => (typeof r === 'string' ? { type: r } : r));
|
|
253
|
+
}
|
|
254
|
+
return rules;
|
|
255
|
+
}
|
|
256
|
+
function normalizeFields(input) {
|
|
257
|
+
const out = [];
|
|
258
|
+
for (const key of Object.keys(input)) {
|
|
259
|
+
const raw = input[key];
|
|
260
|
+
if (typeof raw === 'string' || Array.isArray(raw)) {
|
|
261
|
+
out.push({
|
|
262
|
+
key,
|
|
263
|
+
identifier: key,
|
|
264
|
+
rules: toRuleSpecs(raw),
|
|
265
|
+
optional: false
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const spec = raw;
|
|
270
|
+
out.push({
|
|
271
|
+
key,
|
|
272
|
+
identifier: spec.identifier ?? key,
|
|
273
|
+
rules: toRuleSpecs(spec.rules),
|
|
274
|
+
optional: !!spec.optional,
|
|
275
|
+
depends: spec.depends
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Split `ruleName[param]` (supports nested brackets in regExp by using `value` on rule spec instead). */
|
|
282
|
+
function parseRuleType(type) {
|
|
283
|
+
const t = type.trim();
|
|
284
|
+
const open = t.indexOf('[');
|
|
285
|
+
if (open === -1) {
|
|
286
|
+
return { name: t };
|
|
287
|
+
}
|
|
288
|
+
const close = t.lastIndexOf(']');
|
|
289
|
+
if (close <= open) {
|
|
290
|
+
return { name: t };
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
name: t.slice(0, open),
|
|
294
|
+
bracket: t.slice(open + 1, close)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function asString(v) {
|
|
298
|
+
if (v === null || v === undefined) {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(v)) {
|
|
302
|
+
return v.join(',');
|
|
303
|
+
}
|
|
304
|
+
if (typeof v === 'boolean') {
|
|
305
|
+
return v ? 'true' : '';
|
|
306
|
+
}
|
|
307
|
+
return String(v);
|
|
308
|
+
}
|
|
309
|
+
function isEmptyValue$1(v) {
|
|
310
|
+
if (v === null || v === undefined) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
if (typeof v === 'boolean') {
|
|
314
|
+
return !v;
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(v)) {
|
|
317
|
+
return v.length === 0;
|
|
318
|
+
}
|
|
319
|
+
return String(v).trim() === '';
|
|
320
|
+
}
|
|
321
|
+
const EMAIL_RE = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
322
|
+
function isValidUrl(s) {
|
|
323
|
+
try {
|
|
324
|
+
const u = new URL(s);
|
|
325
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function parseIntegerRange(bracket) {
|
|
332
|
+
if (!bracket) {
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
const m = /^(-?\d+)\.\.(-?\d+)$/.exec(bracket.trim());
|
|
336
|
+
if (m) {
|
|
337
|
+
return { min: Number(m[1]), max: Number(m[2]) };
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function luhnValid(digits) {
|
|
342
|
+
const d = digits.replace(/\D/g, '');
|
|
343
|
+
if (d.length < 13) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
let sum = 0;
|
|
347
|
+
let alt = false;
|
|
348
|
+
for (let i = d.length - 1; i >= 0; i--) {
|
|
349
|
+
let n = parseInt(d.charAt(i), 10);
|
|
350
|
+
if (alt) {
|
|
351
|
+
n *= 2;
|
|
352
|
+
if (n > 9) {
|
|
353
|
+
n -= 9;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
sum += n;
|
|
357
|
+
alt = !alt;
|
|
358
|
+
}
|
|
359
|
+
return sum % 10 === 0;
|
|
360
|
+
}
|
|
361
|
+
const CARD_TYPES = {
|
|
362
|
+
visa: /^4[0-9]{12}(?:[0-9]{3})?$/,
|
|
363
|
+
mastercard: /^5[1-5][0-9]{14}$/,
|
|
364
|
+
amex: /^3[47][0-9]{13}$/,
|
|
365
|
+
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/,
|
|
366
|
+
unionpay: /^62[0-9]{14,17}$/,
|
|
367
|
+
jcb: /^(?:2131|1800|35\d{3})\d{11}$/,
|
|
368
|
+
dinersClub: /^3(?:0[0-5]|[68][0-9])[0-9]{11}$/,
|
|
369
|
+
maestro: /^(?:5[0678]\d{2}|6304|6706|6771|6709)\d{8,15}$/,
|
|
370
|
+
laser: /^(6304|6706|6709|6771)[0-9]{12,15}$/,
|
|
371
|
+
visaElectron: /^(4026|417500|4405|4508|4844|4913|4917)\d+$/
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Returns true if the value satisfies the rule.
|
|
375
|
+
*/
|
|
376
|
+
function evaluateRule(rule, fieldValue, fieldIdentifier, ctx) {
|
|
377
|
+
const { name, bracket } = parseRuleType(rule.type);
|
|
378
|
+
const s = asString(fieldValue);
|
|
379
|
+
const lower = name.toLowerCase();
|
|
380
|
+
switch (lower) {
|
|
381
|
+
case 'empty':
|
|
382
|
+
return !isEmptyValue$1(fieldValue);
|
|
383
|
+
case 'checked':
|
|
384
|
+
return fieldValue === true;
|
|
385
|
+
case 'email':
|
|
386
|
+
return s === '' || EMAIL_RE.test(s);
|
|
387
|
+
case 'url':
|
|
388
|
+
return s === '' || isValidUrl(s);
|
|
389
|
+
case 'decimal':
|
|
390
|
+
return s === '' || /^-?\d+\.\d+$/.test(s.trim());
|
|
391
|
+
case 'number':
|
|
392
|
+
return s === '' || /^-?\d+(\.\d+)?$/.test(s.trim());
|
|
393
|
+
case 'integer': {
|
|
394
|
+
if (s.trim() === '') {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
if (!/^-?\d+$/.test(s.trim())) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
const n = parseInt(s, 10);
|
|
401
|
+
const range = parseIntegerRange(bracket);
|
|
402
|
+
if (range && range.min !== undefined && range.max !== undefined) {
|
|
403
|
+
return n >= range.min && n <= range.max;
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
case 'regexp': {
|
|
408
|
+
let re = null;
|
|
409
|
+
if (rule.value instanceof RegExp) {
|
|
410
|
+
re = rule.value;
|
|
411
|
+
}
|
|
412
|
+
else if (bracket) {
|
|
413
|
+
const inner = bracket.trim();
|
|
414
|
+
const m = inner.match(/^\/(.*)\/([gimuy]*)$/);
|
|
415
|
+
if (m) {
|
|
416
|
+
re = new RegExp(m[1], m[2] || '');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!re) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
return re.test(s);
|
|
423
|
+
}
|
|
424
|
+
case 'minlength': {
|
|
425
|
+
const min = bracket ? parseInt(bracket, 10) : NaN;
|
|
426
|
+
if (Number.isNaN(min)) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return s.length >= min;
|
|
430
|
+
}
|
|
431
|
+
case 'maxlength': {
|
|
432
|
+
const max = bracket ? parseInt(bracket, 10) : NaN;
|
|
433
|
+
if (Number.isNaN(max)) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
return s.length <= max;
|
|
437
|
+
}
|
|
438
|
+
case 'exactlength': {
|
|
439
|
+
const ex = bracket ? parseInt(bracket, 10) : NaN;
|
|
440
|
+
if (Number.isNaN(ex)) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
return s.length === ex;
|
|
444
|
+
}
|
|
445
|
+
case 'match': {
|
|
446
|
+
if (!bracket) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const other = asString(ctx.getFieldValue(bracket));
|
|
450
|
+
return s === other;
|
|
451
|
+
}
|
|
452
|
+
case 'different': {
|
|
453
|
+
if (!bracket) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
const other = asString(ctx.getFieldValue(bracket));
|
|
457
|
+
return s !== other || s === '';
|
|
458
|
+
}
|
|
459
|
+
case 'mincount': {
|
|
460
|
+
const min = bracket ? parseInt(bracket, 10) : NaN;
|
|
461
|
+
if (!Number.isFinite(min)) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
const arr = Array.isArray(fieldValue) ? fieldValue : [];
|
|
465
|
+
return arr.length >= min;
|
|
466
|
+
}
|
|
467
|
+
case 'maxcount': {
|
|
468
|
+
const max = bracket ? parseInt(bracket, 10) : NaN;
|
|
469
|
+
if (!Number.isFinite(max)) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
const arr = Array.isArray(fieldValue) ? fieldValue : [];
|
|
473
|
+
return arr.length <= max;
|
|
474
|
+
}
|
|
475
|
+
case 'exactcount': {
|
|
476
|
+
const ex = bracket ? parseInt(bracket, 10) : NaN;
|
|
477
|
+
if (!Number.isFinite(ex)) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const arr = Array.isArray(fieldValue) ? fieldValue : [];
|
|
481
|
+
return arr.length === ex;
|
|
482
|
+
}
|
|
483
|
+
case 'contains':
|
|
484
|
+
return bracket ? s.toLowerCase().includes(bracket.toLowerCase()) : false;
|
|
485
|
+
case 'containsexactly':
|
|
486
|
+
return bracket ? s.includes(bracket) : false;
|
|
487
|
+
case 'doesntcontain':
|
|
488
|
+
return bracket ? !s.toLowerCase().includes(bracket.toLowerCase()) : false;
|
|
489
|
+
case 'doesntcontainexactly':
|
|
490
|
+
return bracket ? !s.includes(bracket) : false;
|
|
491
|
+
case 'is':
|
|
492
|
+
return bracket ? s.toLowerCase() === bracket.toLowerCase() : false;
|
|
493
|
+
case 'isexactly':
|
|
494
|
+
return bracket ? s === bracket : false;
|
|
495
|
+
case 'not':
|
|
496
|
+
return bracket ? s.toLowerCase() !== bracket.toLowerCase() : false;
|
|
497
|
+
case 'notexactly':
|
|
498
|
+
return bracket ? s !== bracket : false;
|
|
499
|
+
case 'exactly':
|
|
500
|
+
return bracket ? s.toLowerCase() === bracket.toLowerCase() : false;
|
|
501
|
+
case 'creditcard': {
|
|
502
|
+
const digits = s.replace(/\s/g, '');
|
|
503
|
+
if (!luhnValid(digits)) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
if (!bracket) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
const types = bracket.split(',').map(t => t.trim().toLowerCase());
|
|
510
|
+
return types.some(t => {
|
|
511
|
+
const rx = CARD_TYPES[t];
|
|
512
|
+
return rx && rx.test(digits);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
default:
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const DEFAULT_PROMPTS = {
|
|
521
|
+
empty: '{name} must have a value',
|
|
522
|
+
checked: '{name} must be checked',
|
|
523
|
+
email: '{name} must be a valid e-mail',
|
|
524
|
+
url: '{name} must be a valid url',
|
|
525
|
+
regexp: '{name} is not formatted correctly',
|
|
526
|
+
integer: '{name} must be an integer',
|
|
527
|
+
decimal: '{name} must be a decimal number',
|
|
528
|
+
number: '{name} must be set to a number',
|
|
529
|
+
is: "{name} must be '{ruleValue}'",
|
|
530
|
+
isexactly: "{name} must be exactly '{ruleValue}'",
|
|
531
|
+
not: "{name} cannot be set to '{ruleValue}'",
|
|
532
|
+
notexactly: "{name} cannot be set to exactly '{ruleValue}'",
|
|
533
|
+
contains: "{name} must contain '{ruleValue}'",
|
|
534
|
+
containsexactly: "{name} must contain exactly '{ruleValue}'",
|
|
535
|
+
doesntcontain: "{name} must not contain '{ruleValue}'",
|
|
536
|
+
doesntcontainexactly: "{name} must not contain exactly '{ruleValue}'",
|
|
537
|
+
minlength: '{name} must be at least {ruleValue} characters',
|
|
538
|
+
exactlength: '{name} must be exactly {ruleValue} characters',
|
|
539
|
+
maxlength: '{name} cannot be longer than {ruleValue} characters',
|
|
540
|
+
match: '{name} must match {ruleValue} field',
|
|
541
|
+
different: '{name} must have a different value than {ruleValue} field',
|
|
542
|
+
creditcard: '{name} must be a valid credit card number',
|
|
543
|
+
mincount: '{name} must have at least {ruleValue} choices',
|
|
544
|
+
exactcount: '{name} must have exactly {ruleValue} choices',
|
|
545
|
+
maxcount: '{name} must have {ruleValue} or less choices',
|
|
546
|
+
exactly: "{name} must be '{ruleValue}'"
|
|
547
|
+
};
|
|
548
|
+
function formatValidationPrompt(ruleType, fieldName, fieldIdentifier, fieldValue, customPrompt, ruleValue) {
|
|
549
|
+
if (typeof customPrompt === 'function') {
|
|
550
|
+
return customPrompt(fieldValue);
|
|
551
|
+
}
|
|
552
|
+
if (customPrompt) {
|
|
553
|
+
return applyTemplate(customPrompt, fieldName, fieldIdentifier, fieldValue, ruleValue);
|
|
554
|
+
}
|
|
555
|
+
const { name } = parseRuleType(ruleType);
|
|
556
|
+
const key = name.toLowerCase();
|
|
557
|
+
const template = DEFAULT_PROMPTS[key] ?? 'Please enter a valid value';
|
|
558
|
+
return applyTemplate(template, fieldName, fieldIdentifier, fieldValue, ruleValue);
|
|
559
|
+
}
|
|
560
|
+
function applyTemplate(template, name, identifier, value, ruleValue) {
|
|
561
|
+
const v = value === null || value === undefined
|
|
562
|
+
? ''
|
|
563
|
+
: Array.isArray(value)
|
|
564
|
+
? value.join(',')
|
|
565
|
+
: String(value);
|
|
566
|
+
return template
|
|
567
|
+
.replace(/\{name\}/g, name)
|
|
568
|
+
.replace(/\{identifier\}/g, identifier)
|
|
569
|
+
.replace(/\{value\}/g, v)
|
|
570
|
+
.replace(/\{ruleValue\}/g, ruleValue);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Types for Semantic UI [Form Validation](https://semantic-ui.com/behaviors/form.html) — Angular port.
|
|
575
|
+
* Lives with `ngx-semantic/collections/form` (use with `sui-form` + `suiFormValidation` on the same `<form>`).
|
|
576
|
+
*/
|
|
577
|
+
/** `AbstractControl.errors` key for Semantic rule failures (string message). Works with built-in validators. */
|
|
578
|
+
const SUI_FORM_VALIDATION_ERROR_KEY = 'suiFormValidation';
|
|
579
|
+
|
|
580
|
+
/** Merge or clear Semantic validation errors without wiping other validator errors on the control. */
|
|
581
|
+
function patchSuiFormValidationControlError(control, message) {
|
|
582
|
+
const key = SUI_FORM_VALIDATION_ERROR_KEY;
|
|
583
|
+
const cur = control.errors;
|
|
584
|
+
if (message === null) {
|
|
585
|
+
if (!cur?.[key]) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const rest = { ...cur };
|
|
589
|
+
delete rest[key];
|
|
590
|
+
control.setErrors(Object.keys(rest).length ? rest : null);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
control.setErrors({ ...(cur || {}), [key]: message });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function isEmptyValue(v) {
|
|
598
|
+
if (v === null || v === undefined) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
if (typeof v === 'boolean') {
|
|
602
|
+
return !v;
|
|
603
|
+
}
|
|
604
|
+
if (Array.isArray(v)) {
|
|
605
|
+
return v.length === 0;
|
|
606
|
+
}
|
|
607
|
+
return String(v).trim() === '';
|
|
608
|
+
}
|
|
609
|
+
class SuiFormValidationDirective {
|
|
610
|
+
constructor() {
|
|
611
|
+
this.el = inject(ElementRef);
|
|
612
|
+
this.renderer = inject(Renderer2);
|
|
613
|
+
this.zone = inject(NgZone);
|
|
614
|
+
this.ngForm = inject(NgForm, { optional: true, host: true });
|
|
615
|
+
this.formGroupDir = inject(FormGroupDirective, { optional: true, host: true });
|
|
616
|
+
/** Validation field definitions (shorthand or longhand), matching Semantic UI `fields`. */
|
|
617
|
+
this.suiFields = null;
|
|
618
|
+
/** `submit`, `blur`, or `change` — when to validate fields. */
|
|
619
|
+
this.suiOn = 'submit';
|
|
620
|
+
this.suiInline = false;
|
|
621
|
+
this.suiRevalidate = true;
|
|
622
|
+
/** Debounce (ms) for `change` / revalidation; `true` uses 300ms; `false` disables. */
|
|
623
|
+
this.suiDelay = true;
|
|
624
|
+
this.suiKeyboardShortcuts = true;
|
|
625
|
+
/** When using `NgForm` / `FormGroup`, mark all controls touched after a failed submit (shows native errors). */
|
|
626
|
+
this.suiMarkControlsTouchedOnInvalid = true;
|
|
627
|
+
this.suiOnSuccess = new EventEmitter();
|
|
628
|
+
this.suiOnFailure = new EventEmitter();
|
|
629
|
+
this.destroy$ = new Subject();
|
|
630
|
+
this.normalized = [];
|
|
631
|
+
this.defaultValues = new Map();
|
|
632
|
+
this.teardownFieldSubs = null;
|
|
633
|
+
const el = this.el;
|
|
634
|
+
this.host = el.nativeElement;
|
|
635
|
+
}
|
|
636
|
+
/** Reactive `[formGroup]` or template-driven `NgForm` on this host, if any. */
|
|
637
|
+
getAngularForm() {
|
|
638
|
+
if (this.formGroupDir?.form) {
|
|
639
|
+
return this.formGroupDir.form;
|
|
640
|
+
}
|
|
641
|
+
if (this.ngForm?.form) {
|
|
642
|
+
return this.ngForm.form;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
/** Control whose name matches `suiFields` identifier (flat `FormGroup` only). */
|
|
647
|
+
resolveControl(identifier) {
|
|
648
|
+
const g = this.getAngularForm();
|
|
649
|
+
const c = g?.get(identifier);
|
|
650
|
+
return c ?? null;
|
|
651
|
+
}
|
|
652
|
+
ngOnChanges(changes) {
|
|
653
|
+
if (changes['suiFields']) {
|
|
654
|
+
this.normalized = this.suiFields ? normalizeFields(this.suiFields) : [];
|
|
655
|
+
}
|
|
656
|
+
if (changes['suiFields'] || changes['suiOn'] || changes['suiDelay'] || changes['suiRevalidate']) {
|
|
657
|
+
this.bindFieldTriggers();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
ngOnDestroy() {
|
|
661
|
+
this.teardownFieldSubs?.();
|
|
662
|
+
this.destroy$.next();
|
|
663
|
+
this.destroy$.complete();
|
|
664
|
+
}
|
|
665
|
+
onHostSubmit(event) {
|
|
666
|
+
if (!this.normalized.length) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
this.cacheDefaultsOnce();
|
|
670
|
+
const r = this.runValidationWithUi();
|
|
671
|
+
const form = this.getAngularForm();
|
|
672
|
+
const ok = r.valid && (!form || form.valid);
|
|
673
|
+
// Always prevent native form navigation; consumers use suiOnSuccess for follow-up.
|
|
674
|
+
event.preventDefault();
|
|
675
|
+
if (!ok) {
|
|
676
|
+
if (this.suiMarkControlsTouchedOnInvalid && form) {
|
|
677
|
+
form.markAllAsTouched();
|
|
678
|
+
}
|
|
679
|
+
event.stopPropagation();
|
|
680
|
+
const errorsOut = r.errors.length > 0
|
|
681
|
+
? r.errors
|
|
682
|
+
: form && !form.valid
|
|
683
|
+
? ['This form has validation errors.']
|
|
684
|
+
: r.errors;
|
|
685
|
+
this.suiOnFailure.emit({ errors: errorsOut, fields: r.fieldIds });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.suiOnSuccess.emit(event);
|
|
689
|
+
}
|
|
690
|
+
onHostKeydown(ev) {
|
|
691
|
+
if (!this.suiKeyboardShortcuts) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (!this.host.contains(ev.target)) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (ev.key === 'Escape') {
|
|
698
|
+
ev.target?.blur?.();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/** Validate the whole form, update UI, emit success/failure. */
|
|
702
|
+
validateForm() {
|
|
703
|
+
this.cacheDefaultsOnce();
|
|
704
|
+
const r = this.runValidationWithUi();
|
|
705
|
+
const form = this.getAngularForm();
|
|
706
|
+
const ok = r.valid && (!form || form.valid);
|
|
707
|
+
if (!ok) {
|
|
708
|
+
if (this.suiMarkControlsTouchedOnInvalid && form) {
|
|
709
|
+
form.markAllAsTouched();
|
|
710
|
+
}
|
|
711
|
+
const errorsOut = r.errors.length > 0
|
|
712
|
+
? r.errors
|
|
713
|
+
: form && !form.valid
|
|
714
|
+
? ['This form has validation errors.']
|
|
715
|
+
: r.errors;
|
|
716
|
+
this.suiOnFailure.emit({ errors: errorsOut, fields: r.fieldIds });
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
this.suiOnSuccess.emit(new Event('semantic-form-valid'));
|
|
720
|
+
}
|
|
721
|
+
return ok;
|
|
722
|
+
}
|
|
723
|
+
runValidationWithUi() {
|
|
724
|
+
const errors = [];
|
|
725
|
+
const fieldIds = [];
|
|
726
|
+
this.clearFormLevelMessage();
|
|
727
|
+
this.clearAllFieldUi();
|
|
728
|
+
for (const f of this.normalized) {
|
|
729
|
+
const msg = this.evaluateFieldMessage(f);
|
|
730
|
+
const ctrl = this.resolveControl(f.identifier);
|
|
731
|
+
if (msg) {
|
|
732
|
+
errors.push(msg);
|
|
733
|
+
fieldIds.push(f.identifier);
|
|
734
|
+
this.applyFieldError(f.identifier, msg);
|
|
735
|
+
if (ctrl) {
|
|
736
|
+
patchSuiFormValidationControlError(ctrl, msg);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
this.renderFormLevelErrors(errors);
|
|
741
|
+
return { valid: errors.length === 0, errors, fieldIds };
|
|
742
|
+
}
|
|
743
|
+
/** Validate one field by identifier (updates UI and `AbstractControl` errors when bound). */
|
|
744
|
+
validateField(identifier) {
|
|
745
|
+
const field = this.normalized.find(x => x.identifier === identifier || x.key === identifier);
|
|
746
|
+
if (!field) {
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
this.clearFieldUi(field.identifier);
|
|
750
|
+
const msg = this.evaluateFieldMessage(field);
|
|
751
|
+
const ctrl = this.resolveControl(field.identifier);
|
|
752
|
+
if (msg) {
|
|
753
|
+
if (ctrl) {
|
|
754
|
+
patchSuiFormValidationControlError(ctrl, msg);
|
|
755
|
+
}
|
|
756
|
+
this.applyFieldError(field.identifier, msg);
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
/** Whether the form (or one field) passes rules; does not update UI (Semantic `is valid`). */
|
|
762
|
+
isValid(identifier) {
|
|
763
|
+
if (identifier) {
|
|
764
|
+
const field = this.normalized.find(x => x.identifier === identifier || x.key === identifier);
|
|
765
|
+
return field ? this.evaluateFieldMessage(field) === null : true;
|
|
766
|
+
}
|
|
767
|
+
return this.normalized.every(f => this.evaluateFieldMessage(f) === null);
|
|
768
|
+
}
|
|
769
|
+
getFieldElement(identifier) {
|
|
770
|
+
return resolveFieldElement(this.host, identifier);
|
|
771
|
+
}
|
|
772
|
+
getValue(identifier) {
|
|
773
|
+
const ctrl = this.resolveControl(identifier);
|
|
774
|
+
if (ctrl) {
|
|
775
|
+
return ctrl.value;
|
|
776
|
+
}
|
|
777
|
+
const el = this.getFieldElement(identifier);
|
|
778
|
+
return el ? readControlValue(el) : undefined;
|
|
779
|
+
}
|
|
780
|
+
getValues(identifiers) {
|
|
781
|
+
const ids = identifiers ??
|
|
782
|
+
this.normalized.map(f => f.identifier);
|
|
783
|
+
const out = {};
|
|
784
|
+
for (const id of ids) {
|
|
785
|
+
const ctrl = this.resolveControl(id);
|
|
786
|
+
if (ctrl) {
|
|
787
|
+
out[id] = ctrl.value;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
const el = resolveFieldElement(this.host, id);
|
|
791
|
+
if (!el) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const name = el.getAttribute('name') ?? el.getAttribute('id') ?? el.getAttribute('data-validate') ?? id;
|
|
795
|
+
out[name] = readControlValue(el);
|
|
796
|
+
}
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
setValue(identifier, value) {
|
|
800
|
+
const ctrl = this.resolveControl(identifier);
|
|
801
|
+
if (ctrl) {
|
|
802
|
+
ctrl.setValue(value);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const el = resolveFieldElement(this.host, identifier);
|
|
806
|
+
if (!el) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
this.writeControlValue(el, value);
|
|
810
|
+
}
|
|
811
|
+
setValues(values) {
|
|
812
|
+
for (const k of Object.keys(values)) {
|
|
813
|
+
this.setValue(k, values[k]);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/** Restore values cached at first validation init (Semantic `reset`). */
|
|
817
|
+
reset() {
|
|
818
|
+
const fields = this.host.querySelectorAll('input, select, textarea');
|
|
819
|
+
fields.forEach(control => {
|
|
820
|
+
const key = control.name || control.id;
|
|
821
|
+
if (!key) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (this.defaultValues.has(key)) {
|
|
825
|
+
this.writeControlValue(control, this.defaultValues.get(key));
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
/** Clear all field values (Semantic `clear`). */
|
|
830
|
+
clear() {
|
|
831
|
+
this.host.querySelectorAll('input, select, textarea').forEach(control => {
|
|
832
|
+
if (control instanceof HTMLInputElement) {
|
|
833
|
+
if (control.type === 'checkbox' || control.type === 'radio') {
|
|
834
|
+
control.checked = false;
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
control.value = '';
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else if (control instanceof HTMLSelectElement) {
|
|
841
|
+
if (control.multiple) {
|
|
842
|
+
Array.from(control.options).forEach(o => (o.selected = false));
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
control.selectedIndex = 0;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
control.value = '';
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
debounceMs() {
|
|
854
|
+
if (this.suiDelay === false) {
|
|
855
|
+
return 0;
|
|
856
|
+
}
|
|
857
|
+
if (this.suiDelay === true) {
|
|
858
|
+
return 300;
|
|
859
|
+
}
|
|
860
|
+
return Math.max(0, this.suiDelay);
|
|
861
|
+
}
|
|
862
|
+
ctx() {
|
|
863
|
+
return {
|
|
864
|
+
getFieldValue: id => {
|
|
865
|
+
const c = this.resolveControl(id);
|
|
866
|
+
if (c) {
|
|
867
|
+
return c.value;
|
|
868
|
+
}
|
|
869
|
+
const el = resolveFieldElement(this.host, id);
|
|
870
|
+
return el ? readControlValue(el) : '';
|
|
871
|
+
},
|
|
872
|
+
getFieldElement: id => resolveFieldElement(this.host, id)
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
evaluateFieldMessage(field) {
|
|
876
|
+
const ctx = this.ctx();
|
|
877
|
+
if (field.depends) {
|
|
878
|
+
const dep = ctx.getFieldValue(field.depends);
|
|
879
|
+
if (isEmptyValue(dep)) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const ctrl = this.resolveControl(field.identifier);
|
|
884
|
+
const el = resolveFieldElement(this.host, field.identifier);
|
|
885
|
+
const value = ctrl ? ctrl.value : el ? readControlValue(el) : undefined;
|
|
886
|
+
if (field.optional && isEmptyValue(value)) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
const fieldName = el ? inferFieldName(el, field.identifier) : field.identifier;
|
|
890
|
+
for (const rule of field.rules) {
|
|
891
|
+
if (!evaluateRule(rule, value, field.identifier, ctx)) {
|
|
892
|
+
const { bracket } = parseRuleType(rule.type);
|
|
893
|
+
return formatValidationPrompt(rule.type, fieldName, field.identifier, value, rule.prompt, bracket ?? '');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
cacheDefaultsOnce() {
|
|
899
|
+
if (this.defaultValues.size) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
this.host.querySelectorAll('input, select, textarea').forEach(control => {
|
|
903
|
+
const key = control.name || control.id;
|
|
904
|
+
if (!key) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (control instanceof HTMLInputElement && (control.type === 'checkbox' || control.type === 'radio')) {
|
|
908
|
+
this.defaultValues.set(key, control.checked ? '1' : '');
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
this.defaultValues.set(key, control.value);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
bindFieldTriggers() {
|
|
916
|
+
this.teardownFieldSubs?.();
|
|
917
|
+
this.teardownFieldSubs = null;
|
|
918
|
+
if (!this.normalized.length || this.suiOn === 'submit') {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const sub = new Subject();
|
|
922
|
+
const stop = () => {
|
|
923
|
+
sub.next();
|
|
924
|
+
sub.complete();
|
|
925
|
+
};
|
|
926
|
+
this.teardownFieldSubs = stop;
|
|
927
|
+
const ms = this.debounceMs();
|
|
928
|
+
this.zone.runOutsideAngular(() => {
|
|
929
|
+
for (const f of this.normalized) {
|
|
930
|
+
const el = resolveFieldElement(this.host, f.identifier);
|
|
931
|
+
if (!el) {
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const ev = this.suiOn === 'blur'
|
|
935
|
+
? fromEvent(el, 'blur')
|
|
936
|
+
: merge(fromEvent(el, 'input'), fromEvent(el, 'change'));
|
|
937
|
+
const piped = ms > 0 ? ev.pipe(debounceTime(ms)) : ev;
|
|
938
|
+
piped.pipe(takeUntil(sub), takeUntil(this.destroy$)).subscribe(() => {
|
|
939
|
+
this.zone.run(() => {
|
|
940
|
+
this.validateField(f.identifier);
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
if (this.suiRevalidate && this.suiOn === 'blur') {
|
|
946
|
+
this.zone.runOutsideAngular(() => {
|
|
947
|
+
for (const f of this.normalized) {
|
|
948
|
+
const el = resolveFieldElement(this.host, f.identifier);
|
|
949
|
+
if (!el) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const piped = ms > 0
|
|
953
|
+
? fromEvent(el, 'input').pipe(debounceTime(ms))
|
|
954
|
+
: fromEvent(el, 'input');
|
|
955
|
+
piped.pipe(takeUntil(sub), takeUntil(this.destroy$)).subscribe(() => {
|
|
956
|
+
const group = findSemanticField(el);
|
|
957
|
+
if (group?.classList.contains('error')) {
|
|
958
|
+
this.zone.run(() => this.validateField(f.identifier));
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
clearAllFieldUi() {
|
|
966
|
+
for (const f of this.normalized) {
|
|
967
|
+
this.clearFieldUi(f.identifier);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
clearFieldUi(identifier) {
|
|
971
|
+
const ctrl = this.resolveControl(identifier);
|
|
972
|
+
if (ctrl) {
|
|
973
|
+
patchSuiFormValidationControlError(ctrl, null);
|
|
974
|
+
}
|
|
975
|
+
const el = resolveFieldElement(this.host, identifier);
|
|
976
|
+
const group = el ? findSemanticField(el) : null;
|
|
977
|
+
if (group) {
|
|
978
|
+
this.renderer.removeClass(group, 'error');
|
|
979
|
+
const prompt = group.querySelector('.prompt');
|
|
980
|
+
if (prompt) {
|
|
981
|
+
this.renderer.setProperty(prompt, 'textContent', '');
|
|
982
|
+
if (this.suiInline) {
|
|
983
|
+
this.renderer.removeStyle(prompt, 'display');
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
this.renderer.setStyle(prompt, 'display', 'none');
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
applyFieldError(identifier, message) {
|
|
992
|
+
const el = resolveFieldElement(this.host, identifier);
|
|
993
|
+
const group = el ? findSemanticField(el) : null;
|
|
994
|
+
if (!group) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
this.renderer.addClass(group, 'error');
|
|
998
|
+
if (!this.suiInline) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
let prompt = group.querySelector('.prompt');
|
|
1002
|
+
if (!prompt) {
|
|
1003
|
+
prompt = this.renderer.createElement('div');
|
|
1004
|
+
this.renderer.addClass(prompt, 'ui');
|
|
1005
|
+
this.renderer.addClass(prompt, 'basic');
|
|
1006
|
+
this.renderer.addClass(prompt, 'red');
|
|
1007
|
+
this.renderer.addClass(prompt, 'pointing');
|
|
1008
|
+
this.renderer.addClass(prompt, 'label');
|
|
1009
|
+
this.renderer.addClass(prompt, 'prompt');
|
|
1010
|
+
this.renderer.setAttribute(prompt, 'role', 'alert');
|
|
1011
|
+
this.renderer.appendChild(group, prompt);
|
|
1012
|
+
}
|
|
1013
|
+
this.renderer.setProperty(prompt, 'textContent', message);
|
|
1014
|
+
this.renderer.removeStyle(prompt, 'display');
|
|
1015
|
+
}
|
|
1016
|
+
renderFormLevelErrors(messages) {
|
|
1017
|
+
const block = this.host.querySelector('.ui.error.message, .error.message');
|
|
1018
|
+
if (!block || !messages.length) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const ul = this.renderer.createElement('ul');
|
|
1022
|
+
ul.className = 'list';
|
|
1023
|
+
for (const m of messages) {
|
|
1024
|
+
const li = this.renderer.createElement('li');
|
|
1025
|
+
li.textContent = m;
|
|
1026
|
+
ul.appendChild(li);
|
|
1027
|
+
}
|
|
1028
|
+
block.innerHTML = '';
|
|
1029
|
+
block.appendChild(ul);
|
|
1030
|
+
this.renderer.removeClass(block, 'hidden');
|
|
1031
|
+
}
|
|
1032
|
+
clearFormLevelMessage() {
|
|
1033
|
+
const block = this.host.querySelector('.ui.error.message, .error.message');
|
|
1034
|
+
if (block) {
|
|
1035
|
+
block.innerHTML = '';
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
writeControlValue(el, value) {
|
|
1039
|
+
if (el instanceof HTMLInputElement) {
|
|
1040
|
+
if (el.type === 'checkbox') {
|
|
1041
|
+
el.checked = Boolean(value);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (el.type === 'radio') {
|
|
1045
|
+
const form = el.form ?? this.host;
|
|
1046
|
+
const name = el.name;
|
|
1047
|
+
const str = String(value ?? '');
|
|
1048
|
+
form
|
|
1049
|
+
.querySelectorAll(`input[type="radio"][name="${escapeAttrSelector(name)}"]`)
|
|
1050
|
+
.forEach(r => {
|
|
1051
|
+
r.checked = r.value === str;
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
el.value = value === null || value === undefined ? '' : String(value);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (el instanceof HTMLSelectElement) {
|
|
1059
|
+
if (el.multiple && Array.isArray(value)) {
|
|
1060
|
+
const set = new Set(value.map(String));
|
|
1061
|
+
Array.from(el.options).forEach(o => {
|
|
1062
|
+
o.selected = set.has(o.value);
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
el.value = String(value ?? '');
|
|
1067
|
+
}
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
1071
|
+
el.value = value === null || value === undefined ? '' : String(value);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormValidationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
1075
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.7", type: SuiFormValidationDirective, isStandalone: true, selector: "form[suiFormValidation]", inputs: { suiFields: "suiFields", suiOn: "suiOn", suiInline: "suiInline", suiRevalidate: "suiRevalidate", suiDelay: "suiDelay", suiKeyboardShortcuts: "suiKeyboardShortcuts", suiMarkControlsTouchedOnInvalid: "suiMarkControlsTouchedOnInvalid" }, outputs: { suiOnSuccess: "suiOnSuccess", suiOnFailure: "suiOnFailure" }, host: { listeners: { "submit": "onHostSubmit($event)", "keydown": "onHostKeydown($event)" } }, exportAs: ["suiFormValidation"], usesOnChanges: true, ngImport: i0 }); }
|
|
1076
|
+
}
|
|
1077
|
+
__decorate([
|
|
1078
|
+
InputBoolean()
|
|
1079
|
+
], SuiFormValidationDirective.prototype, "suiInline", void 0);
|
|
1080
|
+
__decorate([
|
|
1081
|
+
InputBoolean()
|
|
1082
|
+
], SuiFormValidationDirective.prototype, "suiRevalidate", void 0);
|
|
1083
|
+
__decorate([
|
|
1084
|
+
InputBoolean()
|
|
1085
|
+
], SuiFormValidationDirective.prototype, "suiKeyboardShortcuts", void 0);
|
|
1086
|
+
__decorate([
|
|
1087
|
+
InputBoolean()
|
|
1088
|
+
], SuiFormValidationDirective.prototype, "suiMarkControlsTouchedOnInvalid", void 0);
|
|
1089
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormValidationDirective, decorators: [{
|
|
1090
|
+
type: Directive,
|
|
1091
|
+
args: [{
|
|
1092
|
+
standalone: true,
|
|
1093
|
+
selector: 'form[suiFormValidation]',
|
|
1094
|
+
exportAs: 'suiFormValidation'
|
|
1095
|
+
}]
|
|
1096
|
+
}], ctorParameters: () => [], propDecorators: { suiFields: [{
|
|
1097
|
+
type: Input
|
|
1098
|
+
}], suiOn: [{
|
|
1099
|
+
type: Input
|
|
1100
|
+
}], suiInline: [{
|
|
1101
|
+
type: Input
|
|
1102
|
+
}], suiRevalidate: [{
|
|
1103
|
+
type: Input
|
|
1104
|
+
}], suiDelay: [{
|
|
1105
|
+
type: Input
|
|
1106
|
+
}], suiKeyboardShortcuts: [{
|
|
1107
|
+
type: Input
|
|
1108
|
+
}], suiMarkControlsTouchedOnInvalid: [{
|
|
1109
|
+
type: Input
|
|
1110
|
+
}], suiOnSuccess: [{
|
|
1111
|
+
type: Output
|
|
1112
|
+
}], suiOnFailure: [{
|
|
1113
|
+
type: Output
|
|
1114
|
+
}], onHostSubmit: [{
|
|
1115
|
+
type: HostListener,
|
|
1116
|
+
args: ['submit', ['$event']]
|
|
1117
|
+
}], onHostKeydown: [{
|
|
1118
|
+
type: HostListener,
|
|
1119
|
+
args: ['keydown', ['$event']]
|
|
1120
|
+
}] } });
|
|
1121
|
+
|
|
166
1122
|
/**
|
|
167
1123
|
* Created by bolor on 5/26/2020
|
|
168
1124
|
*/
|
|
169
1125
|
class SuiFormModule {
|
|
170
1126
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
171
1127
|
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.7", ngImport: i0, type: SuiFormModule, imports: [CommonModule,
|
|
1128
|
+
FormsModule,
|
|
1129
|
+
ReactiveFormsModule,
|
|
172
1130
|
SuiFormFieldDirective,
|
|
173
1131
|
SuiFormDirective,
|
|
174
|
-
SuiFormFieldsDirective
|
|
1132
|
+
SuiFormFieldsDirective,
|
|
1133
|
+
SuiFormValidationDirective], exports: [FormsModule,
|
|
1134
|
+
ReactiveFormsModule,
|
|
1135
|
+
SuiFormFieldDirective,
|
|
175
1136
|
SuiFormDirective,
|
|
176
|
-
SuiFormFieldsDirective
|
|
177
|
-
|
|
1137
|
+
SuiFormFieldsDirective,
|
|
1138
|
+
SuiFormValidationDirective] }); }
|
|
1139
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormModule, imports: [CommonModule,
|
|
1140
|
+
FormsModule,
|
|
1141
|
+
ReactiveFormsModule, FormsModule,
|
|
1142
|
+
ReactiveFormsModule] }); }
|
|
178
1143
|
}
|
|
179
1144
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SuiFormModule, decorators: [{
|
|
180
1145
|
type: NgModule,
|
|
181
1146
|
args: [{
|
|
182
1147
|
imports: [
|
|
183
1148
|
CommonModule,
|
|
1149
|
+
FormsModule,
|
|
1150
|
+
ReactiveFormsModule,
|
|
184
1151
|
SuiFormFieldDirective,
|
|
185
1152
|
SuiFormDirective,
|
|
186
|
-
SuiFormFieldsDirective
|
|
1153
|
+
SuiFormFieldsDirective,
|
|
1154
|
+
SuiFormValidationDirective
|
|
187
1155
|
],
|
|
188
1156
|
exports: [
|
|
1157
|
+
FormsModule,
|
|
1158
|
+
ReactiveFormsModule,
|
|
189
1159
|
SuiFormFieldDirective,
|
|
190
1160
|
SuiFormDirective,
|
|
191
|
-
SuiFormFieldsDirective
|
|
1161
|
+
SuiFormFieldsDirective,
|
|
1162
|
+
SuiFormValidationDirective
|
|
192
1163
|
]
|
|
193
1164
|
}]
|
|
194
1165
|
}] });
|
|
@@ -201,5 +1172,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImpor
|
|
|
201
1172
|
* Generated bundle index. Do not edit.
|
|
202
1173
|
*/
|
|
203
1174
|
|
|
204
|
-
export { SuiFormDirective, SuiFormFieldDirective, SuiFormFieldsDirective, SuiFormModule };
|
|
1175
|
+
export { SUI_FORM_VALIDATION_ERROR_KEY, SuiFormDirective, SuiFormFieldDirective, SuiFormFieldsDirective, SuiFormModule, SuiFormValidationDirective, escapeAttrSelector, evaluateRule, findSemanticField, formatValidationPrompt, inferFieldName, normalizeFields, parseRuleType, patchSuiFormValidationControlError, readControlValue, resolveFieldElement };
|
|
205
1176
|
//# sourceMappingURL=ngx-semantic-collections-form.mjs.map
|