@windwalker-io/unicorn-next 0.1.4 → 0.1.6

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.
Files changed (129) hide show
  1. package/.editorconfig +18 -18
  2. package/.gulp.json +7 -7
  3. package/bin/release.mjs +47 -47
  4. package/dist/chunks/button-radio.js.map +1 -1
  5. package/dist/chunks/checkboxes-multi-select.js.map +1 -1
  6. package/dist/chunks/field-cascade-select.js.map +1 -1
  7. package/dist/chunks/field-file-drag.js.map +1 -1
  8. package/dist/chunks/field-flatpickr.js.map +1 -1
  9. package/dist/chunks/field-modal-select.js.map +1 -1
  10. package/dist/chunks/field-modal-tree.js +2 -2
  11. package/dist/chunks/field-modal-tree.js.map +1 -1
  12. package/dist/chunks/field-multi-uploader.js.map +1 -1
  13. package/dist/chunks/field-repeatable.js.map +1 -1
  14. package/dist/chunks/field-single-image-drag.js.map +1 -1
  15. package/dist/chunks/form.js.map +1 -1
  16. package/dist/chunks/grid.js.map +1 -1
  17. package/dist/chunks/http-client.js.map +1 -1
  18. package/dist/chunks/iframe-modal.js.map +1 -1
  19. package/dist/chunks/keep-tab.js.map +1 -1
  20. package/dist/chunks/legacy.js +2 -2
  21. package/dist/chunks/legacy.js.map +1 -1
  22. package/dist/chunks/list-dependent.js.map +1 -1
  23. package/dist/chunks/s3-multipart-uploader.js.map +1 -1
  24. package/dist/chunks/s3-uploader.js.map +1 -1
  25. package/dist/chunks/show-on.js.map +1 -1
  26. package/dist/chunks/tinymce.js +1 -1
  27. package/dist/chunks/tinymce.js.map +1 -1
  28. package/dist/chunks/ui-bootstrap5.js.map +1 -1
  29. package/dist/chunks/unicorn.js +22 -16
  30. package/dist/chunks/unicorn.js.map +1 -1
  31. package/dist/chunks/validation.js +1 -1
  32. package/dist/chunks/validation.js.map +1 -1
  33. package/dist/index.d.ts +39 -30
  34. package/dist/unicorn.js +11 -9
  35. package/fusionfile.mjs +155 -155
  36. package/package.json +103 -103
  37. package/scss/bootstrap/multi-level-menu.scss +121 -121
  38. package/scss/editor.scss +116 -116
  39. package/scss/field/file-drag.scss +102 -102
  40. package/scss/field/single-image-drag.scss +88 -88
  41. package/scss/field/vue-drag-uploader.scss +160 -160
  42. package/scss/switcher.scss +156 -156
  43. package/src/app.ts +128 -128
  44. package/src/bootstrap/button-radio.ts +208 -208
  45. package/src/bootstrap/keep-tab.ts +155 -155
  46. package/src/composable/index.ts +22 -21
  47. package/src/composable/useCheckboxesMultiSelect.ts +22 -22
  48. package/src/composable/useFieldCascadeSelect.ts +9 -9
  49. package/src/composable/useFieldFileDrag.ts +9 -9
  50. package/src/composable/useFieldFlatpickr.ts +3 -3
  51. package/src/composable/useFieldModalSelect.ts +6 -6
  52. package/src/composable/useFieldModalTree.ts +3 -3
  53. package/src/composable/useFieldMultiUploader.ts +9 -9
  54. package/src/composable/useFieldRepeatable.ts +9 -9
  55. package/src/composable/useFieldSingleImageDrag.ts +9 -9
  56. package/src/composable/useForm.ts +43 -43
  57. package/src/composable/useGrid.ts +57 -57
  58. package/src/composable/useHttp.ts +9 -9
  59. package/src/composable/useIframeModal.ts +10 -10
  60. package/src/composable/useListDependent.ts +26 -26
  61. package/src/composable/useQueue.ts +13 -13
  62. package/src/composable/useS3Uploader.ts +32 -32
  63. package/src/composable/useShowOn.ts +9 -9
  64. package/src/composable/useStack.ts +13 -13
  65. package/src/composable/useTinymce.ts +29 -29
  66. package/src/composable/useTomSelect.ts +72 -72
  67. package/src/composable/useUIBootstrap5.ts +48 -48
  68. package/src/composable/useUniDirective.ts +32 -32
  69. package/src/composable/useValidation.ts +50 -39
  70. package/src/data.ts +34 -34
  71. package/src/events.ts +82 -82
  72. package/src/legacy/legacy.ts +190 -190
  73. package/src/legacy/loader.ts +125 -125
  74. package/src/module/checkboxes-multi-select.ts +54 -54
  75. package/src/module/field-cascade-select.ts +292 -292
  76. package/src/module/field-file-drag.ts +295 -295
  77. package/src/module/field-flatpickr.ts +130 -130
  78. package/src/module/field-modal-select.ts +179 -179
  79. package/src/module/field-modal-tree.ts +31 -31
  80. package/src/module/field-multi-uploader.ts +368 -368
  81. package/src/module/field-repeatable.ts +202 -202
  82. package/src/module/field-single-image-drag.ts +475 -475
  83. package/src/module/form.ts +223 -223
  84. package/src/module/grid.ts +465 -465
  85. package/src/module/http-client.ts +248 -248
  86. package/src/module/iframe-modal.ts +170 -170
  87. package/src/module/list-dependent.ts +321 -321
  88. package/src/module/s3-multipart-uploader.ts +300 -300
  89. package/src/module/s3-uploader.ts +234 -234
  90. package/src/module/show-on.ts +175 -175
  91. package/src/module/tinymce.ts +276 -276
  92. package/src/module/ui-bootstrap5.ts +116 -116
  93. package/src/module/validation.ts +1046 -1026
  94. package/src/plugin/index.ts +1 -1
  95. package/src/plugin/php-adapter.ts +72 -72
  96. package/src/polyfill/form-request-submit.ts +31 -31
  97. package/src/polyfill/index.ts +9 -9
  98. package/src/service/animate.ts +58 -58
  99. package/src/service/crypto.ts +27 -27
  100. package/src/service/dom-watcher.ts +62 -62
  101. package/src/service/dom.ts +265 -265
  102. package/src/service/helper.ts +48 -48
  103. package/src/service/index.ts +10 -10
  104. package/src/service/lang.ts +122 -122
  105. package/src/service/loader.ts +152 -152
  106. package/src/service/router.ts +118 -118
  107. package/src/service/ui.ts +525 -525
  108. package/src/service/uri.ts +106 -106
  109. package/src/types/base.ts +9 -9
  110. package/src/types/index.ts +4 -4
  111. package/src/types/modal-tree.ts +12 -12
  112. package/src/types/plugin.ts +6 -6
  113. package/src/types/shims.d.ts +18 -18
  114. package/src/types/ui.ts +6 -6
  115. package/src/unicorn.ts +79 -79
  116. package/src/utilities/arr.ts +25 -25
  117. package/src/utilities/base.ts +9 -9
  118. package/src/utilities/data.ts +48 -48
  119. package/src/utilities/index.ts +5 -5
  120. package/src/utilities/tree.ts +20 -20
  121. package/src/vue/components/ModalTree/ModalTreeApp.vue +175 -175
  122. package/src/vue/components/ModalTree/TreeItem.vue +262 -262
  123. package/src/vue/components/ModalTree/TreeModal.vue +225 -225
  124. package/tests/test.js +4 -4
  125. package/tsconfig.js.json +25 -25
  126. package/tsconfig.json +17 -17
  127. package/vite.assets.config.ts +61 -61
  128. package/vite.config.test.ts +36 -36
  129. package/vite.config.ts +112 -112
@@ -1,1026 +1,1046 @@
1
- import { useUniDirective } from '../composable';
2
- import { getBoundedInstance, html, selectAll, selectOne, trans, useUITheme } from '../service';
3
- import { Nullable } from '../types';
4
- import { mergeDeep } from '../utilities';
5
- import * as punycode from 'punycode';
6
-
7
- export declare type ValidationHandler = (value: any, input: HTMLElement, options?: Record<string, any>, fv?: UnicornFieldValidation) => any;
8
-
9
- export declare type Validator = {
10
- handler: ValidationHandler,
11
- options?: Record<string, any>;
12
- };
13
-
14
- const validatorHandlers: Record<string, ValidationHandler> = {};
15
-
16
- type InputElements = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
17
-
18
- export interface FormValidationOptions {
19
- scroll: boolean;
20
- validatedClass: null;
21
- fieldSelector: null;
22
- scrollOffset: number;
23
- enabled: boolean;
24
- }
25
-
26
- export interface FieldValidationOptions {
27
- validClass: string;
28
- errorSelector: string;
29
- inputOptions: boolean;
30
- inputOptionsSelector: string;
31
- formSelector: string;
32
- selector: string;
33
- inputOptionsWrapperSelector: string;
34
- events: string[];
35
- invalidClass: string;
36
- errorMessageClass: string;
37
- }
38
-
39
- const defaultOptions: FormValidationOptions = {
40
- scroll: false,
41
- scrollOffset: -100,
42
- enabled: true,
43
- fieldSelector: null,
44
- validatedClass: null,
45
- };
46
-
47
- const defaultFieldOptions: FieldValidationOptions = {
48
- formSelector: '[uni-form-validate]',
49
- errorSelector: '[data-field-error]',
50
- selector: 'input[data-field-input], select[data-field-input], textarea[data-field-input]',
51
- validClass: 'is-valid',
52
- invalidClass: 'is-invalid',
53
- events: ['change'],
54
- errorMessageClass: 'invalid-tooltip',
55
- inputOptions: false,
56
- inputOptionsWrapperSelector: 'div[data-field-input]',
57
- inputOptionsSelector: '[data-input-option]'
58
- };
59
-
60
- export class UnicornFormValidation {
61
- presetFields: HTMLElement[] = [];
62
-
63
- static globalValidators: Record<string, Validator> = {};
64
-
65
- validators: Record<string, Validator> = {};
66
- options: FormValidationOptions;
67
- $form: HTMLElement;
68
-
69
- static is = 'uni-form-validate';
70
-
71
- constructor(el: HTMLElement, options: Partial<FormValidationOptions> = {}) {
72
- this.$form = selectOne(el);
73
- this.options = this.mergeOptions(options);
74
-
75
- this.registerDefaultValidators();
76
-
77
- this.init();
78
- }
79
-
80
- mergeOptions(options: Partial<FormValidationOptions>) {
81
- // Fix PHP empty array to JSON issue.
82
- if (Array.isArray(options)) {
83
- options = {};
84
- }
85
-
86
- return this.options = mergeDeep({}, defaultOptions, options);
87
- }
88
-
89
- get scrollEnabled() {
90
- return this.options.scroll;
91
- }
92
-
93
- get scrollOffset() {
94
- return Number(this.options.scrollOffset || -100);
95
- }
96
-
97
- get fieldSelector() {
98
- return this.options.fieldSelector || 'input, select, textarea';
99
- }
100
-
101
- get validatedClass() {
102
- return this.options.validatedClass || 'was-validated';
103
- }
104
-
105
- init() {
106
- if (this.$form.tagName === 'FORM') {
107
- this.$form.setAttribute('novalidate', 'true');
108
- this.$form.addEventListener('submit', (event) => {
109
- if (this.options.enabled && !this.validateAll()) {
110
- event.stopImmediatePropagation(); // Stop following events
111
- event.stopPropagation();
112
- event.preventDefault();
113
-
114
- this.$form.dispatchEvent(new CustomEvent('invalid'));
115
-
116
- return false;
117
- }
118
-
119
- return true;
120
- }, false);
121
- }
122
-
123
- this.prepareFields(this.findDOMFields());
124
- this.prepareFields(this.presetFields);
125
- }
126
-
127
- findDOMFields(): HTMLElement[] {
128
- return selectAll(this.$form.querySelectorAll<HTMLElement>(this.fieldSelector));
129
- }
130
-
131
- prepareFields(inputs: HTMLElement[]): Promise<void> {
132
- inputs.forEach((input) => {
133
- this.prepareFieldWrapper(input);
134
- });
135
-
136
- // Wait next tick
137
- return Promise.resolve();
138
- }
139
-
140
- prepareFieldWrapper(input: HTMLElement): HTMLElement | null {
141
- if (['INPUT', 'SELECT', 'TEXTAREA'].indexOf(input.tagName) !== -1) {
142
- let wrapper: HTMLElement | null = input.closest('[uni-field-validate]');
143
-
144
- if (!wrapper) {
145
- wrapper = input.closest('[data-input-container]') || input.parentElement;
146
-
147
- wrapper?.setAttribute('uni-field-validate', '{}');
148
- }
149
-
150
- return wrapper;
151
- }
152
-
153
- return input;
154
- }
155
-
156
- findFields(containsPresets: boolean = true): HTMLElement[] {
157
- let inputs = this.findDOMFields();
158
-
159
- if (containsPresets) {
160
- inputs.push(...this.presetFields);
161
- }
162
-
163
- return inputs.map((input) => this.prepareFieldWrapper(input))
164
- .filter(input => input != null) as HTMLElement[];
165
- }
166
-
167
- getFieldComponent(input: HTMLElement): UnicornFieldValidation | null {
168
- let v = getBoundedInstance(input, 'field.validation');
169
-
170
- if (!v) {
171
- const wrapper = input.closest('[uni-field-validate]') as HTMLElement | null;
172
-
173
- if (wrapper) {
174
- v = getBoundedInstance(wrapper, 'field.validation');
175
- }
176
- }
177
-
178
- return v;
179
- }
180
-
181
- validateAll(fields?: Nullable<HTMLElement[]>): boolean {
182
- this.markFormAsUnvalidated();
183
-
184
- fields = fields || this.findFields();
185
- let firstFail: HTMLElement | null = null;
186
-
187
- for (const field of fields) {
188
- const fv = this.getFieldComponent(field);
189
-
190
- if (!fv) {
191
- continue;
192
- }
193
-
194
- const result = fv.checkValidity();
195
-
196
- if (!result && !firstFail) {
197
- firstFail = field;
198
- }
199
- }
200
-
201
- this.markFormAsValidated();
202
-
203
- if (firstFail && this.scrollEnabled) {
204
- this.scrollTo(firstFail);
205
- }
206
-
207
- return firstFail === null;
208
- }
209
-
210
- async validateAllAsync(fields?: Nullable<HTMLElement[]>): Promise<boolean> {
211
- this.markFormAsUnvalidated();
212
-
213
- fields = fields || this.findFields();
214
- let firstFail: HTMLElement | null = null;
215
- const promises: Promise<boolean>[] = [];
216
-
217
- for (const field of fields) {
218
- const fv = this.getFieldComponent(field);
219
-
220
- if (!fv) {
221
- continue;
222
- }
223
-
224
- promises.push(
225
- fv.checkValidityAsync().then((result) => {
226
- if (!result && !firstFail) {
227
- firstFail = field;
228
- }
229
-
230
- return result;
231
- })
232
- );
233
- }
234
-
235
- await Promise.all(promises);
236
-
237
- this.markFormAsValidated();
238
-
239
- if (firstFail && this.scrollEnabled) {
240
- this.scrollTo(firstFail);
241
- }
242
-
243
- return firstFail === null;
244
- }
245
-
246
- scrollTo(element: HTMLElement): void {
247
- const offset = this.scrollOffset;
248
- const elementPosition = element.getBoundingClientRect().top;
249
- const offsetPosition = elementPosition + window.scrollY + offset;
250
-
251
- window.scrollTo({
252
- top: offsetPosition,
253
- behavior: 'smooth'
254
- });
255
- }
256
-
257
- markFormAsValidated(): void {
258
- if (!this.$form) {
259
- return;
260
- }
261
-
262
- this.$form.classList.add(this.validatedClass);
263
- }
264
-
265
- markFormAsUnvalidated(): void {
266
- if (!this.$form) {
267
- return;
268
- }
269
-
270
- this.$form.classList.remove(this.validatedClass);
271
- }
272
-
273
- addField(field: HTMLElement): this {
274
- this.presetFields.push(field);
275
-
276
- this.prepareFieldWrapper(field);
277
-
278
- return this;
279
- }
280
-
281
- registerDefaultValidators(): void {
282
- for (let name in validatorHandlers) {
283
- this.addValidator(name, validatorHandlers[name]);
284
- }
285
- }
286
-
287
- /**
288
- * Add validator handler.
289
- */
290
- addValidator(name: string, handler: ValidationHandler, options: Record<string, any> = {}) {
291
- options = options || {};
292
-
293
- this.validators[name] = {
294
- handler,
295
- options
296
- };
297
-
298
- return this;
299
- }
300
-
301
- /**
302
- * Add validator handler.
303
- */
304
- static addGlobalValidator(name: string, handler: ValidationHandler, options: Record<string, any> = {}) {
305
- options = options || {};
306
-
307
- this.globalValidators[name] = {
308
- handler,
309
- options
310
- };
311
-
312
- return this;
313
- }
314
- }
315
-
316
- export class UnicornFieldValidation {
317
- $input: InputElements | undefined;
318
- options: FieldValidationOptions;
319
-
320
- static is = 'uni-field-validate';
321
-
322
- constructor(protected el: HTMLElement, options: Partial<FieldValidationOptions> = {}) {
323
- this.options = this.mergeOptions(options);
324
-
325
- this.$input = this.selectInput();
326
-
327
- this.init();
328
- }
329
-
330
- mergeOptions(options: Partial<FieldValidationOptions>) {
331
- // Fix PHP empty array to JSON issue.
332
- if (Array.isArray(options)) {
333
- options = {};
334
- }
335
-
336
- return this.options = mergeDeep({}, defaultFieldOptions, options);
337
- }
338
-
339
- get $form(): HTMLFormElement {
340
- return this.getForm();
341
- }
342
-
343
- get errorSelector(): string {
344
- return this.options.errorSelector;
345
- }
346
-
347
- get selector(): string {
348
- return this.options.selector;
349
- }
350
-
351
- get validClass(): string {
352
- return this.options.validClass;
353
- }
354
-
355
- get invalidClass(): string {
356
- return this.options.invalidClass;
357
- }
358
-
359
- get isVisible(): boolean {
360
- return !!(this.el.offsetWidth || this.el.offsetHeight || this.el.getClientRects().length);
361
- }
362
-
363
- get isInputOptions(): boolean {
364
- return Boolean(this.options.inputOptions);
365
- }
366
-
367
- get validationMessage(): string {
368
- return this.$input?.validationMessage || '';
369
- }
370
-
371
- get validity(): ValidityState | undefined {
372
- return this.$input?.validity;
373
- }
374
-
375
- selectInput(): InputElements | undefined {
376
- let selector = this.selector;
377
-
378
- if (this.options.inputOptions) {
379
- selector += ', ' + this.options.inputOptionsWrapperSelector;
380
- }
381
-
382
- let input = this.el.querySelector<InputElements>(selector);
383
-
384
- if (!input) {
385
- input = this.el.querySelector<InputElements>('input, select, textarea');
386
- }
387
-
388
- if (!input) {
389
- return undefined;
390
- }
391
-
392
- return this.$input = input;
393
- }
394
-
395
- init() {
396
- this.selectInput();
397
-
398
- this.bindEvents();
399
-
400
- this.prepareWrapper();
401
-
402
- if (this.isInputOptions) {
403
- const $input = this.$input as any;
404
-
405
- if (
406
- !($input instanceof HTMLInputElement)
407
- && !($input instanceof HTMLSelectElement)
408
- && !($input instanceof HTMLTextAreaElement)
409
- ) {
410
- $input.validationMessage = '';
411
-
412
- $input.setCustomValidity = (msg: string) => {
413
- $input.validationMessage = String(msg);
414
- };
415
-
416
- $input.checkValidity = () => {
417
- return this.checkInputOptionsValidity();
418
- };
419
- }
420
- }
421
- }
422
-
423
- bindEvents() {
424
- if (!this.$input) {
425
- return;
426
- }
427
-
428
- this.$input.addEventListener('invalid', (e) => {
429
- this.showInvalidResponse();
430
- });
431
-
432
- const events = this.options.events;
433
-
434
- events.forEach((eventName) => {
435
- this.$input?.addEventListener(eventName, () => {
436
- this.checkValidity();
437
- });
438
- });
439
- }
440
-
441
- prepareWrapper() {
442
- if (this.el.querySelector(this.errorSelector)?.classList?.contains('invalid-tooltip')) {
443
- if (window.getComputedStyle(this.el).position === 'static') {
444
- this.el.style.position = 'relative';
445
- }
446
- }
447
- }
448
-
449
- checkValidity() {
450
- if (!this.$input) {
451
- return true;
452
- }
453
-
454
- if (this.$input.hasAttribute('readonly')) {
455
- return true;
456
- }
457
-
458
- if (this.$input.hasAttribute('[data-novalidate]')) {
459
- return true;
460
- }
461
-
462
- if (this.$input.closest('[data-novalidate]')) {
463
- return true;
464
- }
465
-
466
- this.$input.setCustomValidity('');
467
- let valid = this.$input.checkValidity();
468
-
469
- if (valid && this.$form) {
470
- valid = this.runCustomValidity();
471
- }
472
-
473
- // Raise invalid event
474
- // this.$input.checkValidity();
475
-
476
- this.updateValidClass(valid);
477
-
478
- return valid;
479
- }
480
-
481
- runCustomValidity() {
482
- if (!this.$input) {
483
- return true;
484
- }
485
-
486
- // Check custom validity
487
- const validates = (this.$input.getAttribute('data-validate') || '').split('|');
488
- let result = true;
489
-
490
- if (this.$input.value !== '' && validates.length) {
491
- if (!this.checkCustomDataAttributeValidity()) {
492
- return false;
493
- }
494
-
495
- for (const validatorName of validates) {
496
- const [validator, options] = this.getValidator(validatorName) || [null, {}];
497
-
498
- if (!validator) {
499
- continue;
500
- }
501
-
502
- Object.assign(options, validator.options);
503
-
504
- let r = validator.handler(this.$input.value, this.$input, options, this);
505
-
506
- // If return is a promise, push to stack and resolve later
507
- if (r instanceof Promise || (typeof r === 'object' && r.then)) {
508
- r.then((result: boolean) => {
509
- this.handleAsyncCustomResult(result, validator);
510
- });
511
- continue;
512
- }
513
-
514
- if (!this.handleCustomResult(r, validator)) {
515
- result = false;
516
-
517
- break;
518
- }
519
- }
520
- }
521
-
522
- return result;
523
- }
524
-
525
- async checkValidityAsync() {
526
- if (!this.$input) {
527
- return true;
528
- }
529
-
530
- if (this.$input.hasAttribute('readonly')) {
531
- return true;
532
- }
533
-
534
- this.$input.setCustomValidity('');
535
- let valid = this.$input.checkValidity();
536
-
537
- if (valid && this.$form) {
538
- valid = await this.runCustomValidityAsync();
539
- }
540
-
541
- this.updateValidClass(valid);
542
-
543
- return valid;
544
- }
545
-
546
- async runCustomValidityAsync(): Promise<boolean> {
547
- if (!this.$input) {
548
- return true;
549
- }
550
-
551
- // Check custom validity
552
- const validates = (this.$input.getAttribute('data-validate') || '').split('|');
553
-
554
- const results: Array<boolean | string | undefined> = [];
555
- const promises: Promise<boolean>[] = [];
556
-
557
- if (this.$input.value !== '' && validates.length) {
558
- if (!this.checkCustomDataAttributeValidity()) {
559
- return false;
560
- }
561
-
562
- for (const validatorName of validates) {
563
- let [validator, options] = this.getValidator(validatorName) || [null, {}];
564
-
565
- if (!validator) {
566
- continue;
567
- }
568
-
569
- options = Object.assign({}, options, validator.options || {});
570
-
571
- promises.push(
572
- Promise.resolve(validator.handler(this.$input.value, this.$input, options, this))
573
- .then((r) => {
574
- results.push(this.handleAsyncCustomResult(r, validator));
575
-
576
- return r;
577
- })
578
- );
579
- }
580
- }
581
-
582
- await Promise.all(promises);
583
-
584
- for (const result of results) {
585
- if (result === false) {
586
- return false;
587
- }
588
- }
589
-
590
- return true;
591
- }
592
-
593
- checkCustomDataAttributeValidity(): boolean {
594
- const error = this.$input?.dataset.validationFail;
595
-
596
- return this.handleCustomResult(error);
597
- }
598
-
599
- checkInputOptionsValidity(): boolean {
600
- if (!this.$input) {
601
- return true;
602
- }
603
-
604
- const isRequired = this.$input.getAttribute('required') != null;
605
- const optionWrappers = this.$input.querySelectorAll(this.options.inputOptionsSelector);
606
- let result = true;
607
-
608
- if (isRequired) {
609
- for (const optionWrapper of optionWrappers) {
610
- const input = optionWrapper.querySelector('input');
611
-
612
- result = false;
613
-
614
- // Only need one checked
615
- if (input?.checked) {
616
- result = true;
617
- break;
618
- }
619
- }
620
- }
621
-
622
- // Get browser input validation message
623
- const n = document.createElement('input');
624
- n.required = isRequired;
625
-
626
- if (result) {
627
- n.value = 'placeholder';
628
- }
629
-
630
- n.checkValidity();
631
-
632
- (this.$input as any).validationMessage = n.validationMessage;
633
- (this.$input as any).validity = n.validity;
634
-
635
- for (const optionWrapper of optionWrappers) {
636
- const input = optionWrapper.querySelector<HTMLInputElement>('input');
637
-
638
- input?.setCustomValidity(n.validationMessage);
639
- }
640
-
641
- if (!result) {
642
- this.$input.dispatchEvent(
643
- new CustomEvent('invalid')
644
- );
645
- }
646
-
647
- return result;
648
- }
649
-
650
- /**
651
- * @param valid {boolean}
652
- */
653
- updateValidClass(valid: Boolean) {
654
- const $errorElement = this.getErrorElement();
655
- const $invalidTarget = $errorElement?.previousElementSibling;
656
-
657
- this.$input?.classList.remove(this.invalidClass);
658
- this.$input?.classList.remove(this.validClass);
659
- this.el.classList.remove(this.invalidClass);
660
- this.el.classList.remove(this.validClass);
661
- $invalidTarget?.classList.remove(this.invalidClass);
662
- $invalidTarget?.classList.remove(this.validClass);
663
-
664
- if (valid) {
665
- this.$input?.classList.add(this.validClass);
666
- this.el.classList.add(this.validClass);
667
-
668
- $invalidTarget?.classList.add(this.validClass);
669
- } else {
670
- this.$input?.classList.add(this.invalidClass);
671
- this.el.classList.add(this.invalidClass);
672
-
673
- $invalidTarget?.classList.add(this.invalidClass);
674
- }
675
- }
676
-
677
- getFormValidation(element?: Nullable<HTMLFormElement>): UnicornFormValidation | null {
678
- return getBoundedInstance(element || this.getForm(), 'form.validation')!;
679
- }
680
-
681
- getValidator(name: string): [Validator, Record<string, any>] | null {
682
- const matches = name.match(/(?<type>[\w\-_]+)(\((?<params>.*)\))*/);
683
-
684
- if (!matches) {
685
- return null;
686
- }
687
-
688
- const validatorName = matches.groups?.type || '';
689
-
690
- const params = matches.groups?.params || '';
691
-
692
- const fv = this.getFormValidation(this.$form!);
693
- const validator = fv?.validators[validatorName] || UnicornFormValidation.globalValidators[validatorName];
694
-
695
- if (!validator) {
696
- return null;
697
- }
698
-
699
- const paramMatches = params.matchAll(/(?<key>\w+)(\s?[=:]\s?(?<value>\w+))?/g);
700
- const options: Record<string, string> = {};
701
-
702
- for (const paramMatch of paramMatches) {
703
- const match = paramMatch?.groups as {
704
- key: string;
705
- value: string;
706
- } | undefined;
707
-
708
- if (!match) {
709
- continue;
710
- }
711
-
712
- options[match.key] = handleParamValue(match.value);
713
- }
714
-
715
- return [ validator, options ];
716
- }
717
-
718
- handleCustomResult(result: boolean | string | undefined, validator?: Nullable<Validator>): boolean {
719
- if (typeof result === 'string') {
720
- this.$input?.setCustomValidity(result);
721
- result = result === '';
722
- } else if (result === undefined) {
723
- result = true;
724
- }
725
-
726
- if (result) {
727
- this.$input?.setCustomValidity('');
728
- } else if (validator) {
729
- this.raiseCustomErrorState(validator);
730
- }
731
-
732
- return result;
733
- }
734
-
735
- handleAsyncCustomResult(result: boolean, validator?: Nullable<Validator>): boolean {
736
- result = this.handleCustomResult(result, validator);
737
-
738
- // Fire invalid events
739
- this.$input?.checkValidity();
740
-
741
- this.updateValidClass(result);
742
-
743
- return result;
744
- }
745
-
746
- raiseCustomErrorState(validator: Validator): void {
747
- let help;
748
-
749
- if (this.$input?.validationMessage === '') {
750
- help = validator.options?.notice;
751
-
752
- if (typeof help === 'function') {
753
- help = help(this.$input, this);
754
- }
755
-
756
- if (help != null) {
757
- this.$input?.setCustomValidity(help);
758
- }
759
- }
760
-
761
- if (this.$input?.validationMessage === '') {
762
- this.$input?.setCustomValidity(trans('unicorn.message.validation.custom.error'));
763
- }
764
-
765
- this.$input?.dispatchEvent(
766
- new CustomEvent('invalid')
767
- );
768
- }
769
-
770
- setAsInvalidAndReport(error: string) {
771
- this.setCustomValidity(error);
772
- this.showInvalidResponse();
773
- }
774
-
775
- setCustomValidity(error: string) {
776
- this.$input?.setCustomValidity(error);
777
- }
778
-
779
- reportValidity() {
780
- if (this.validationMessage !== '') {
781
- this.showInvalidResponse();
782
- }
783
- }
784
-
785
- showInvalidResponse() {
786
- /** @type ValidityState */
787
- const state = this.$input?.validity;
788
- let message: string = this.$input?.validationMessage || '';
789
-
790
- for (let key in state) {
791
- if (state[(key as keyof ValidityState)] && this.$input?.dataset[key + 'Message']) {
792
- message = this.$input?.dataset[key + 'Message'] || '';
793
- break;
794
- }
795
- }
796
-
797
- if (!this.isVisible) {
798
- let title = this.findLabel()?.textContent;
799
-
800
- if (!title) {
801
- title = this.$input?.name || '';
802
- }
803
-
804
- useUITheme().renderMessage(
805
- `Field: ${title} - ${message}`,
806
- 'warning'
807
- );
808
- }
809
-
810
- let $help = this.getErrorElement();
811
-
812
- if (!$help) {
813
- $help = this.createHelpElement()!;
814
- this.el.appendChild($help);
815
- this.prepareWrapper();
816
- }
817
-
818
- $help.textContent = message;
819
-
820
- this.updateValidClass(false);
821
- }
822
-
823
- getErrorElement() {
824
- return this.el.querySelector(this.errorSelector);
825
- }
826
-
827
- createHelpElement() {
828
- const className = this.options.errorMessageClass;
829
- const parsed = this.parseSelector(this.errorSelector || '');
830
-
831
- const $help = html(`<div class="${className}"></div>`)!;
832
-
833
- $help.classList.add(...parsed.classes);
834
-
835
- parsed.attrs.forEach((attr) => {
836
- $help.setAttribute(attr[0], attr[1] || '');
837
- });
838
-
839
- parsed.ids.forEach((id) => {
840
- $help.id = id;
841
- });
842
-
843
- return $help;
844
- }
845
-
846
- /**
847
- * @see https://stackoverflow.com/a/17888178
848
- */
849
- parseSelector(subselector: string): { tags: string[]; classes: string[]; ids: string[]; attrs: string[][] } {
850
- const obj: {
851
- tags: string[];
852
- classes: string[];
853
- ids: string[];
854
- attrs: string[][];
855
- } = { tags: [], classes: [], ids: [], attrs: [] };
856
- for (const token of subselector.split(/(?=\.)|(?=#)|(?=\[)/)) {
857
- switch (token[0]) {
858
- case '#':
859
- obj.ids.push(token.slice(1));
860
- break;
861
- case '.':
862
- obj.classes.push(token.slice(1));
863
- break;
864
- case '[':
865
- obj.attrs.push(token.slice(1, -1).split('='));
866
- break;
867
- default :
868
- obj.tags.push(token);
869
- break;
870
- }
871
- }
872
- return obj;
873
- }
874
-
875
- setAsValidAndClearResponse() {
876
- this.setCustomValidity('');
877
- this.updateValidClass(true);
878
- this.clearInvalidResponse();
879
- }
880
-
881
- clearInvalidResponse() {
882
- const $help = this.el.querySelector(this.errorSelector)!;
883
-
884
- $help.textContent = '';
885
- }
886
-
887
- getForm() {
888
- return this.el.closest(this.options.formSelector || '[uni-form-validate]') as HTMLFormElement;
889
- }
890
-
891
- findLabel() {
892
- const id = this.$input?.id || '';
893
-
894
- const wrapper = this.$input?.closest('[data-field-wrapper]');
895
- let label = null;
896
-
897
- if (wrapper) {
898
- label = wrapper.querySelector('[data-field-label]');
899
- }
900
-
901
- if (!label) {
902
- label = document.querySelector(`label[for="${id}"]`);
903
- }
904
-
905
- return label;
906
- }
907
- }
908
-
909
- function camelTo(str: string, sep: string) {
910
- return str.replace(/([a-z])([A-Z])/g, `$1${sep}$2`).toLowerCase();
911
- }
912
-
913
- validatorHandlers.username = function (value: any, element: HTMLElement) {
914
- const regex = new RegExp('[\<|\>|"|\'|\%|\;|\(|\)|\&]', 'i');
915
- return !regex.test(value);
916
- };
917
-
918
- validatorHandlers.numeric = function (value: any, element: HTMLElement) {
919
- const regex = /^(\d|-)?(\d|,)*\.?\d*$/;
920
- return regex.test(value);
921
- };
922
-
923
- validatorHandlers.email = function (value: any, element: HTMLElement) {
924
- value = punycode.toASCII(value);
925
- const regex = /^[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
926
- return regex.test(value);
927
- };
928
-
929
- validatorHandlers.url = function (value: any, element: HTMLElement) {
930
- const regex = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i;
931
- return regex.test(value);
932
- };
933
-
934
- validatorHandlers.alnum = function (value: any, element: HTMLElement) {
935
- const regex = /^[a-zA-Z0-9]*$/;
936
- return regex.test(value);
937
- };
938
-
939
- validatorHandlers.color = function (value: any, element: HTMLElement) {
940
- const regex = /^#(?:[0-9a-f]{3}){1,2}$/;
941
- return regex.test(value);
942
- };
943
-
944
- /**
945
- * @see http://www.virtuosimedia.com/dev/php/37-tested-php-perl-and-javascript-regular-expressions
946
- */
947
- validatorHandlers.creditcard = function (value: any, element: HTMLElement) {
948
- const regex = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|622((12[6-9]|1[3-9][0-9])|([2-8][0-9][0-9])|(9(([0-1][0-9])|(2[0-5]))))[0-9]{10}|64[4-9][0-9]{13}|65[0-9]{14}|3(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})*$/;
949
- return regex.test(value);
950
- };
951
-
952
- validatorHandlers.ip = function (value: any, element: HTMLElement) {
953
- const regex = /^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))*$/;
954
- return regex.test(value);
955
- };
956
-
957
- validatorHandlers['password-confirm'] = function (value: any, element: HTMLElement) {
958
- const selector = element.dataset.confirmTarget;
959
-
960
- if (!selector) {
961
- throw new Error('Validator: "password-confirm" must add "data-confirm-target" attribute.');
962
- }
963
-
964
- const target = document.querySelector<HTMLInputElement>(selector);
965
-
966
- return target?.value === value;
967
- };
968
-
969
- export { validatorHandlers as validators };
970
-
971
- // customElements.define(UnicornFormValidateElement.is, UnicornFormValidateElement);
972
- // customElements.define(UnicornFieldValidateElement.is, UnicornFieldValidateElement);
973
-
974
- export const ready = Promise.all([
975
- useUniDirective('form-validate', {
976
- mounted(el, binding) {
977
- getBoundedInstance(el, 'form.validation', (ele) => {
978
- return new UnicornFormValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
979
- });
980
- },
981
- updated(el, binding) {
982
- const instance = getBoundedInstance<UnicornFormValidation>(el, 'form.validation');
983
- instance.mergeOptions(JSON.parse(binding.value || '{}'));
984
- }
985
- }),
986
-
987
- useUniDirective('field-validate', {
988
- mounted(el, binding) {
989
- getBoundedInstance<UnicornFieldValidation>(el, 'field.validation', (ele) => {
990
- return new UnicornFieldValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
991
- });
992
- },
993
-
994
- updated(el, binding) {
995
- const instance = getBoundedInstance<UnicornFieldValidation>(el, 'field.validation');
996
- instance.mergeOptions(JSON.parse(binding.value || '{}') || {});
997
- }
998
- })
999
- ]);
1000
-
1001
- function handleParamValue(value: any) {
1002
- if (!isNaN(Number(value))) {
1003
- return Number(value);
1004
- }
1005
-
1006
- if (value === 'null') {
1007
- return null;
1008
- }
1009
-
1010
- if (value === 'true') {
1011
- return true;
1012
- }
1013
-
1014
- if (value === 'false') {
1015
- return true;
1016
- }
1017
-
1018
- return value;
1019
- }
1020
-
1021
- export interface ValidationModule {
1022
- UnicornFormValidation: typeof UnicornFormValidation;
1023
- UnicornFieldValidation: typeof UnicornFieldValidation;
1024
- ready: Promise<any>;
1025
- validators: typeof validatorHandlers;
1026
- }
1
+ import * as punycode from 'punycode';
2
+ import { useUniDirective } from '../composable';
3
+ import { getBoundedInstance, html, selectAll, selectOne, trans, useUITheme } from '../service';
4
+ import { Nullable } from '../types';
5
+ import { mergeDeep } from '../utilities';
6
+
7
+ export declare type ValidationHandler<V = any, E = HTMLElement, P = Record<string, any>> = (
8
+ value: V,
9
+ input: E,
10
+ options?: ValidatorOptions<E, P>,
11
+ fv?: UnicornFieldValidation
12
+ ) => any;
13
+
14
+ export declare type ValidatorNoticeFunction<E = HTMLElement> = (input: E, field: UnicornFieldValidation) => any;
15
+
16
+ export type ValidatorOptions<E = HTMLElement, P = Record<string, any>> = {
17
+ notice?: ValidatorNoticeFunction<E> | string;
18
+ [name: string]: any;
19
+ } & Partial<P>;
20
+
21
+ export declare type Validator<V = any, E = HTMLElement, P = Record<string, any>> = {
22
+ handler: ValidationHandler<V, E, P>,
23
+ options?: ValidatorOptions<E, P>;
24
+ };
25
+
26
+ const validatorHandlers: Record<string, ValidationHandler<any, any>> = {};
27
+
28
+ type InputElements = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
29
+
30
+ export interface FormValidationOptions {
31
+ scroll: boolean;
32
+ validatedClass: null;
33
+ fieldSelector: null;
34
+ scrollOffset: number;
35
+ enabled: boolean;
36
+ }
37
+
38
+ export interface FieldValidationOptions {
39
+ validClass: string;
40
+ errorSelector: string;
41
+ inputOptions: boolean;
42
+ inputOptionsSelector: string;
43
+ formSelector: string;
44
+ selector: string;
45
+ inputOptionsWrapperSelector: string;
46
+ events: string[];
47
+ invalidClass: string;
48
+ errorMessageClass: string;
49
+ }
50
+
51
+ const defaultOptions: FormValidationOptions = {
52
+ scroll: false,
53
+ scrollOffset: -100,
54
+ enabled: true,
55
+ fieldSelector: null,
56
+ validatedClass: null,
57
+ };
58
+
59
+ const defaultFieldOptions: FieldValidationOptions = {
60
+ formSelector: '[uni-form-validate]',
61
+ errorSelector: '[data-field-error]',
62
+ selector: 'input[data-field-input], select[data-field-input], textarea[data-field-input]',
63
+ validClass: 'is-valid',
64
+ invalidClass: 'is-invalid',
65
+ events: ['change'],
66
+ errorMessageClass: 'invalid-tooltip',
67
+ inputOptions: false,
68
+ inputOptionsWrapperSelector: 'div[data-field-input]',
69
+ inputOptionsSelector: '[data-input-option]'
70
+ };
71
+
72
+ export class UnicornFormValidation {
73
+ presetFields: HTMLElement[] = [];
74
+
75
+ static globalValidators: Record<string, Validator<any, any, any>> = {};
76
+
77
+ validators: Record<string, Validator<any, any, any>> = {};
78
+ options: FormValidationOptions;
79
+ $form: HTMLElement;
80
+
81
+ static is = 'uni-form-validate';
82
+
83
+ constructor(el: HTMLElement, options: Partial<FormValidationOptions> = {}) {
84
+ this.$form = selectOne(el);
85
+ this.options = this.mergeOptions(options);
86
+
87
+ this.registerDefaultValidators();
88
+
89
+ this.init();
90
+ }
91
+
92
+ mergeOptions(options: Partial<FormValidationOptions>) {
93
+ // Fix PHP empty array to JSON issue.
94
+ if (Array.isArray(options)) {
95
+ options = {};
96
+ }
97
+
98
+ return this.options = mergeDeep({}, defaultOptions, options);
99
+ }
100
+
101
+ get scrollEnabled() {
102
+ return this.options.scroll;
103
+ }
104
+
105
+ get scrollOffset() {
106
+ return Number(this.options.scrollOffset || -100);
107
+ }
108
+
109
+ get fieldSelector() {
110
+ return this.options.fieldSelector || 'input, select, textarea';
111
+ }
112
+
113
+ get validatedClass() {
114
+ return this.options.validatedClass || 'was-validated';
115
+ }
116
+
117
+ init() {
118
+ if (this.$form.tagName === 'FORM') {
119
+ this.$form.setAttribute('novalidate', 'true');
120
+ this.$form.addEventListener('submit', (event) => {
121
+ if (this.options.enabled && !this.validateAll()) {
122
+ event.stopImmediatePropagation(); // Stop following events
123
+ event.stopPropagation();
124
+ event.preventDefault();
125
+
126
+ this.$form.dispatchEvent(new CustomEvent('invalid'));
127
+
128
+ return false;
129
+ }
130
+
131
+ return true;
132
+ }, false);
133
+ }
134
+
135
+ this.prepareFields(this.findDOMFields());
136
+ this.prepareFields(this.presetFields);
137
+ }
138
+
139
+ findDOMFields(): HTMLElement[] {
140
+ return selectAll(this.$form.querySelectorAll<HTMLElement>(this.fieldSelector));
141
+ }
142
+
143
+ prepareFields(inputs: HTMLElement[]): Promise<void> {
144
+ inputs.forEach((input) => {
145
+ this.prepareFieldWrapper(input);
146
+ });
147
+
148
+ // Wait next tick
149
+ return Promise.resolve();
150
+ }
151
+
152
+ prepareFieldWrapper(input: HTMLElement): HTMLElement | null {
153
+ if (['INPUT', 'SELECT', 'TEXTAREA'].indexOf(input.tagName) !== -1) {
154
+ let wrapper: HTMLElement | null = input.closest('[uni-field-validate]');
155
+
156
+ if (!wrapper) {
157
+ wrapper = input.closest('[data-input-container]') || input.parentElement;
158
+
159
+ wrapper?.setAttribute('uni-field-validate', '{}');
160
+ }
161
+
162
+ return wrapper;
163
+ }
164
+
165
+ return input;
166
+ }
167
+
168
+ findFields(containsPresets: boolean = true): HTMLElement[] {
169
+ let inputs = this.findDOMFields();
170
+
171
+ if (containsPresets) {
172
+ inputs.push(...this.presetFields);
173
+ }
174
+
175
+ return inputs.map((input) => this.prepareFieldWrapper(input))
176
+ .filter(input => input != null) as HTMLElement[];
177
+ }
178
+
179
+ getFieldComponent(input: HTMLElement): UnicornFieldValidation | null {
180
+ let v = getBoundedInstance(input, 'field.validation');
181
+
182
+ if (!v) {
183
+ const wrapper = input.closest('[uni-field-validate]') as HTMLElement | null;
184
+
185
+ if (wrapper) {
186
+ v = getBoundedInstance(wrapper, 'field.validation');
187
+ }
188
+ }
189
+
190
+ return v;
191
+ }
192
+
193
+ validateAll(fields?: Nullable<HTMLElement[]>): boolean {
194
+ this.markFormAsUnvalidated();
195
+
196
+ fields = fields || this.findFields();
197
+ let firstFail: HTMLElement | null = null;
198
+
199
+ for (const field of fields) {
200
+ const fv = this.getFieldComponent(field);
201
+
202
+ if (!fv) {
203
+ continue;
204
+ }
205
+
206
+ const result = fv.checkValidity();
207
+
208
+ if (!result && !firstFail) {
209
+ firstFail = field;
210
+ }
211
+ }
212
+
213
+ this.markFormAsValidated();
214
+
215
+ if (firstFail && this.scrollEnabled) {
216
+ this.scrollTo(firstFail);
217
+ }
218
+
219
+ return firstFail === null;
220
+ }
221
+
222
+ async validateAllAsync(fields?: Nullable<HTMLElement[]>): Promise<boolean> {
223
+ this.markFormAsUnvalidated();
224
+
225
+ fields = fields || this.findFields();
226
+ let firstFail: HTMLElement | null = null;
227
+ const promises: Promise<boolean>[] = [];
228
+
229
+ for (const field of fields) {
230
+ const fv = this.getFieldComponent(field);
231
+
232
+ if (!fv) {
233
+ continue;
234
+ }
235
+
236
+ promises.push(
237
+ fv.checkValidityAsync().then((result) => {
238
+ if (!result && !firstFail) {
239
+ firstFail = field;
240
+ }
241
+
242
+ return result;
243
+ })
244
+ );
245
+ }
246
+
247
+ await Promise.all(promises);
248
+
249
+ this.markFormAsValidated();
250
+
251
+ if (firstFail && this.scrollEnabled) {
252
+ this.scrollTo(firstFail);
253
+ }
254
+
255
+ return firstFail === null;
256
+ }
257
+
258
+ scrollTo(element: HTMLElement): void {
259
+ const offset = this.scrollOffset;
260
+ const elementPosition = element.getBoundingClientRect().top;
261
+ const offsetPosition = elementPosition + window.scrollY + offset;
262
+
263
+ window.scrollTo({
264
+ top: offsetPosition,
265
+ behavior: 'smooth'
266
+ });
267
+ }
268
+
269
+ markFormAsValidated(): void {
270
+ if (!this.$form) {
271
+ return;
272
+ }
273
+
274
+ this.$form.classList.add(this.validatedClass);
275
+ }
276
+
277
+ markFormAsUnvalidated(): void {
278
+ if (!this.$form) {
279
+ return;
280
+ }
281
+
282
+ this.$form.classList.remove(this.validatedClass);
283
+ }
284
+
285
+ addField(field: HTMLElement): this {
286
+ this.presetFields.push(field);
287
+
288
+ this.prepareFieldWrapper(field);
289
+
290
+ return this;
291
+ }
292
+
293
+ registerDefaultValidators(): void {
294
+ for (let name in validatorHandlers) {
295
+ this.addValidator(name, validatorHandlers[name]);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Add validator handler.
301
+ */
302
+ addValidator<T extends any, E extends HTMLElement, P = Record<string, any>>(
303
+ name: string,
304
+ handler: ValidationHandler<T, E, P>,
305
+ options: ValidatorOptions<E, P> = {}
306
+ ) {
307
+ options = options || {};
308
+
309
+ this.validators[name] = {
310
+ handler,
311
+ options
312
+ };
313
+
314
+ return this;
315
+ }
316
+
317
+ /**
318
+ * Add validator handler.
319
+ */
320
+ static addGlobalValidator<T extends any, E extends HTMLElement, P = Record<string, any>>(
321
+ name: string,
322
+ handler: ValidationHandler<T, E, P>,
323
+ options: ValidatorOptions<E, P> = {}
324
+ ) {
325
+ options = options || {};
326
+
327
+ this.globalValidators[name] = {
328
+ handler,
329
+ options
330
+ };
331
+
332
+ return this;
333
+ }
334
+ }
335
+
336
+ export class UnicornFieldValidation {
337
+ $input: InputElements | undefined;
338
+ options: FieldValidationOptions;
339
+
340
+ static is = 'uni-field-validate';
341
+
342
+ constructor(protected el: HTMLElement, options: Partial<FieldValidationOptions> = {}) {
343
+ this.options = this.mergeOptions(options);
344
+
345
+ this.$input = this.selectInput();
346
+
347
+ this.init();
348
+ }
349
+
350
+ mergeOptions(options: Partial<FieldValidationOptions>) {
351
+ // Fix PHP empty array to JSON issue.
352
+ if (Array.isArray(options)) {
353
+ options = {};
354
+ }
355
+
356
+ return this.options = mergeDeep({}, defaultFieldOptions, options);
357
+ }
358
+
359
+ get $form(): HTMLFormElement {
360
+ return this.getForm();
361
+ }
362
+
363
+ get errorSelector(): string {
364
+ return this.options.errorSelector;
365
+ }
366
+
367
+ get selector(): string {
368
+ return this.options.selector;
369
+ }
370
+
371
+ get validClass(): string {
372
+ return this.options.validClass;
373
+ }
374
+
375
+ get invalidClass(): string {
376
+ return this.options.invalidClass;
377
+ }
378
+
379
+ get isVisible(): boolean {
380
+ return !!(this.el.offsetWidth || this.el.offsetHeight || this.el.getClientRects().length);
381
+ }
382
+
383
+ get isInputOptions(): boolean {
384
+ return Boolean(this.options.inputOptions);
385
+ }
386
+
387
+ get validationMessage(): string {
388
+ return this.$input?.validationMessage || '';
389
+ }
390
+
391
+ get validity(): ValidityState | undefined {
392
+ return this.$input?.validity;
393
+ }
394
+
395
+ selectInput(): InputElements | undefined {
396
+ let selector = this.selector;
397
+
398
+ if (this.options.inputOptions) {
399
+ selector += ', ' + this.options.inputOptionsWrapperSelector;
400
+ }
401
+
402
+ let input = this.el.querySelector<InputElements>(selector);
403
+
404
+ if (!input) {
405
+ input = this.el.querySelector<InputElements>('input, select, textarea');
406
+ }
407
+
408
+ if (!input) {
409
+ return undefined;
410
+ }
411
+
412
+ return this.$input = input;
413
+ }
414
+
415
+ init() {
416
+ this.selectInput();
417
+
418
+ this.bindEvents();
419
+
420
+ this.prepareWrapper();
421
+
422
+ if (this.isInputOptions) {
423
+ const $input = this.$input as any;
424
+
425
+ if (
426
+ !($input instanceof HTMLInputElement)
427
+ && !($input instanceof HTMLSelectElement)
428
+ && !($input instanceof HTMLTextAreaElement)
429
+ ) {
430
+ $input.validationMessage = '';
431
+
432
+ $input.setCustomValidity = (msg: string) => {
433
+ $input.validationMessage = String(msg);
434
+ };
435
+
436
+ $input.checkValidity = () => {
437
+ return this.checkInputOptionsValidity();
438
+ };
439
+ }
440
+ }
441
+ }
442
+
443
+ bindEvents() {
444
+ if (!this.$input) {
445
+ return;
446
+ }
447
+
448
+ this.$input.addEventListener('invalid', (e) => {
449
+ this.showInvalidResponse();
450
+ });
451
+
452
+ const events = this.options.events;
453
+
454
+ events.forEach((eventName) => {
455
+ this.$input?.addEventListener(eventName, () => {
456
+ this.checkValidity();
457
+ });
458
+ });
459
+ }
460
+
461
+ prepareWrapper() {
462
+ if (this.el.querySelector(this.errorSelector)?.classList?.contains('invalid-tooltip')) {
463
+ if (window.getComputedStyle(this.el).position === 'static') {
464
+ this.el.style.position = 'relative';
465
+ }
466
+ }
467
+ }
468
+
469
+ checkValidity() {
470
+ if (!this.$input) {
471
+ return true;
472
+ }
473
+
474
+ if (this.$input.hasAttribute('readonly')) {
475
+ return true;
476
+ }
477
+
478
+ if (this.$input.hasAttribute('[data-novalidate]')) {
479
+ return true;
480
+ }
481
+
482
+ if (this.$input.closest('[data-novalidate]')) {
483
+ return true;
484
+ }
485
+
486
+ this.$input.setCustomValidity('');
487
+ let valid = this.$input.checkValidity();
488
+
489
+ if (valid && this.$form) {
490
+ valid = this.runCustomValidity();
491
+ }
492
+
493
+ // Raise invalid event
494
+ // this.$input.checkValidity();
495
+
496
+ this.updateValidClass(valid);
497
+
498
+ return valid;
499
+ }
500
+
501
+ runCustomValidity() {
502
+ if (!this.$input) {
503
+ return true;
504
+ }
505
+
506
+ // Check custom validity
507
+ const validates = (this.$input.getAttribute('data-validate') || '').split('|');
508
+ let result = true;
509
+
510
+ if (this.$input.value !== '' && validates.length) {
511
+ if (!this.checkCustomDataAttributeValidity()) {
512
+ return false;
513
+ }
514
+
515
+ for (const validatorName of validates) {
516
+ const [validator, options] = this.getValidator(validatorName) || [null, {}];
517
+
518
+ if (!validator) {
519
+ continue;
520
+ }
521
+
522
+ Object.assign(options, validator.options);
523
+
524
+ let r = validator.handler(this.$input.value, this.$input, options, this);
525
+
526
+ // If return is a promise, push to stack and resolve later
527
+ if (r instanceof Promise || (typeof r === 'object' && r.then)) {
528
+ r.then((result: boolean) => {
529
+ this.handleAsyncCustomResult(result, validator);
530
+ });
531
+ continue;
532
+ }
533
+
534
+ if (!this.handleCustomResult(r, validator)) {
535
+ result = false;
536
+
537
+ break;
538
+ }
539
+ }
540
+ }
541
+
542
+ return result;
543
+ }
544
+
545
+ async checkValidityAsync() {
546
+ if (!this.$input) {
547
+ return true;
548
+ }
549
+
550
+ if (this.$input.hasAttribute('readonly')) {
551
+ return true;
552
+ }
553
+
554
+ this.$input.setCustomValidity('');
555
+ let valid = this.$input.checkValidity();
556
+
557
+ if (valid && this.$form) {
558
+ valid = await this.runCustomValidityAsync();
559
+ }
560
+
561
+ this.updateValidClass(valid);
562
+
563
+ return valid;
564
+ }
565
+
566
+ async runCustomValidityAsync(): Promise<boolean> {
567
+ if (!this.$input) {
568
+ return true;
569
+ }
570
+
571
+ // Check custom validity
572
+ const validates = (this.$input.getAttribute('data-validate') || '').split('|');
573
+
574
+ const results: Array<boolean | string | undefined> = [];
575
+ const promises: Promise<boolean>[] = [];
576
+
577
+ if (this.$input.value !== '' && validates.length) {
578
+ if (!this.checkCustomDataAttributeValidity()) {
579
+ return false;
580
+ }
581
+
582
+ for (const validatorName of validates) {
583
+ let [validator, options] = this.getValidator(validatorName) || [null, {}];
584
+
585
+ if (!validator) {
586
+ continue;
587
+ }
588
+
589
+ options = Object.assign({}, options, validator.options || {});
590
+
591
+ promises.push(
592
+ Promise.resolve(validator.handler(this.$input.value, this.$input, options, this))
593
+ .then((r) => {
594
+ results.push(this.handleAsyncCustomResult(r, validator));
595
+
596
+ return r;
597
+ })
598
+ );
599
+ }
600
+ }
601
+
602
+ await Promise.all(promises);
603
+
604
+ for (const result of results) {
605
+ if (result === false) {
606
+ return false;
607
+ }
608
+ }
609
+
610
+ return true;
611
+ }
612
+
613
+ checkCustomDataAttributeValidity(): boolean {
614
+ const error = this.$input?.dataset.validationFail;
615
+
616
+ return this.handleCustomResult(error);
617
+ }
618
+
619
+ checkInputOptionsValidity(): boolean {
620
+ if (!this.$input) {
621
+ return true;
622
+ }
623
+
624
+ const isRequired = this.$input.getAttribute('required') != null;
625
+ const optionWrappers = this.$input.querySelectorAll(this.options.inputOptionsSelector);
626
+ let result = true;
627
+
628
+ if (isRequired) {
629
+ for (const optionWrapper of optionWrappers) {
630
+ const input = optionWrapper.querySelector('input');
631
+
632
+ result = false;
633
+
634
+ // Only need one checked
635
+ if (input?.checked) {
636
+ result = true;
637
+ break;
638
+ }
639
+ }
640
+ }
641
+
642
+ // Get browser input validation message
643
+ const n = document.createElement('input');
644
+ n.required = isRequired;
645
+
646
+ if (result) {
647
+ n.value = 'placeholder';
648
+ }
649
+
650
+ n.checkValidity();
651
+
652
+ (this.$input as any).validationMessage = n.validationMessage;
653
+ (this.$input as any).validity = n.validity;
654
+
655
+ for (const optionWrapper of optionWrappers) {
656
+ const input = optionWrapper.querySelector<HTMLInputElement>('input');
657
+
658
+ input?.setCustomValidity(n.validationMessage);
659
+ }
660
+
661
+ if (!result) {
662
+ this.$input.dispatchEvent(
663
+ new CustomEvent('invalid')
664
+ );
665
+ }
666
+
667
+ return result;
668
+ }
669
+
670
+ /**
671
+ * @param valid {boolean}
672
+ */
673
+ updateValidClass(valid: Boolean) {
674
+ const $errorElement = this.getErrorElement();
675
+ const $invalidTarget = $errorElement?.previousElementSibling;
676
+
677
+ this.$input?.classList.remove(this.invalidClass);
678
+ this.$input?.classList.remove(this.validClass);
679
+ this.el.classList.remove(this.invalidClass);
680
+ this.el.classList.remove(this.validClass);
681
+ $invalidTarget?.classList.remove(this.invalidClass);
682
+ $invalidTarget?.classList.remove(this.validClass);
683
+
684
+ if (valid) {
685
+ this.$input?.classList.add(this.validClass);
686
+ this.el.classList.add(this.validClass);
687
+
688
+ $invalidTarget?.classList.add(this.validClass);
689
+ } else {
690
+ this.$input?.classList.add(this.invalidClass);
691
+ this.el.classList.add(this.invalidClass);
692
+
693
+ $invalidTarget?.classList.add(this.invalidClass);
694
+ }
695
+ }
696
+
697
+ getFormValidation(element?: Nullable<HTMLFormElement>): UnicornFormValidation | null {
698
+ return getBoundedInstance(element || this.getForm(), 'form.validation')!;
699
+ }
700
+
701
+ getValidator(name: string): [Validator, Record<string, any>] | null {
702
+ const matches = name.match(/(?<type>[\w\-_]+)(\((?<params>.*)\))*/);
703
+
704
+ if (!matches) {
705
+ return null;
706
+ }
707
+
708
+ const validatorName = matches.groups?.type || '';
709
+
710
+ const params = matches.groups?.params || '';
711
+
712
+ const fv = this.getFormValidation(this.$form!);
713
+ const validator = fv?.validators[validatorName] || UnicornFormValidation.globalValidators[validatorName];
714
+
715
+ if (!validator) {
716
+ return null;
717
+ }
718
+
719
+ const paramMatches = params.matchAll(/(?<key>\w+)(\s?[=:]\s?(?<value>\w+))?/g);
720
+ const options: Record<string, string> = {};
721
+
722
+ for (const paramMatch of paramMatches) {
723
+ const match = paramMatch?.groups as {
724
+ key: string;
725
+ value: string;
726
+ } | undefined;
727
+
728
+ if (!match) {
729
+ continue;
730
+ }
731
+
732
+ options[match.key] = handleParamValue(match.value);
733
+ }
734
+
735
+ return [validator, options];
736
+ }
737
+
738
+ handleCustomResult(result: boolean | string | undefined, validator?: Nullable<Validator>): boolean {
739
+ if (typeof result === 'string') {
740
+ this.$input?.setCustomValidity(result);
741
+ result = result === '';
742
+ } else if (result === undefined) {
743
+ result = true;
744
+ }
745
+
746
+ if (result) {
747
+ this.$input?.setCustomValidity('');
748
+ } else if (validator) {
749
+ this.raiseCustomErrorState(validator);
750
+ }
751
+
752
+ return result;
753
+ }
754
+
755
+ handleAsyncCustomResult(result: boolean, validator?: Nullable<Validator>): boolean {
756
+ result = this.handleCustomResult(result, validator);
757
+
758
+ // Fire invalid events
759
+ this.$input?.checkValidity();
760
+
761
+ this.updateValidClass(result);
762
+
763
+ return result;
764
+ }
765
+
766
+ raiseCustomErrorState(validator: Validator): void {
767
+ let help;
768
+
769
+ if (this.$input?.validationMessage === '') {
770
+ help = validator.options?.notice;
771
+
772
+ if (typeof help === 'function') {
773
+ help = help(this.$input, this);
774
+ }
775
+
776
+ if (help != null) {
777
+ this.$input?.setCustomValidity(help);
778
+ }
779
+ }
780
+
781
+ if (this.$input?.validationMessage === '') {
782
+ this.$input?.setCustomValidity(trans('unicorn.message.validation.custom.error'));
783
+ }
784
+
785
+ this.$input?.dispatchEvent(
786
+ new CustomEvent('invalid')
787
+ );
788
+ }
789
+
790
+ setAsInvalidAndReport(error: string) {
791
+ this.setCustomValidity(error);
792
+ this.showInvalidResponse();
793
+ }
794
+
795
+ setCustomValidity(error: string) {
796
+ this.$input?.setCustomValidity(error);
797
+ }
798
+
799
+ reportValidity() {
800
+ if (this.validationMessage !== '') {
801
+ this.showInvalidResponse();
802
+ }
803
+ }
804
+
805
+ showInvalidResponse() {
806
+ /** @type ValidityState */
807
+ const state = this.$input?.validity;
808
+ let message: string = this.$input?.validationMessage || '';
809
+
810
+ for (let key in state) {
811
+ if (state[(key as keyof ValidityState)] && this.$input?.dataset[key + 'Message']) {
812
+ message = this.$input?.dataset[key + 'Message'] || '';
813
+ break;
814
+ }
815
+ }
816
+
817
+ if (!this.isVisible) {
818
+ let title = this.findLabel()?.textContent;
819
+
820
+ if (!title) {
821
+ title = this.$input?.name || '';
822
+ }
823
+
824
+ useUITheme().renderMessage(
825
+ `Field: ${title} - ${message}`,
826
+ 'warning'
827
+ );
828
+ }
829
+
830
+ let $help = this.getErrorElement();
831
+
832
+ if (!$help) {
833
+ $help = this.createHelpElement()!;
834
+ this.el.appendChild($help);
835
+ this.prepareWrapper();
836
+ }
837
+
838
+ $help.textContent = message;
839
+
840
+ this.updateValidClass(false);
841
+ }
842
+
843
+ getErrorElement() {
844
+ return this.el.querySelector(this.errorSelector);
845
+ }
846
+
847
+ createHelpElement() {
848
+ const className = this.options.errorMessageClass;
849
+ const parsed = this.parseSelector(this.errorSelector || '');
850
+
851
+ const $help = html(`<div class="${className}"></div>`)!;
852
+
853
+ $help.classList.add(...parsed.classes);
854
+
855
+ parsed.attrs.forEach((attr) => {
856
+ $help.setAttribute(attr[0], attr[1] || '');
857
+ });
858
+
859
+ parsed.ids.forEach((id) => {
860
+ $help.id = id;
861
+ });
862
+
863
+ return $help;
864
+ }
865
+
866
+ /**
867
+ * @see https://stackoverflow.com/a/17888178
868
+ */
869
+ parseSelector(subselector: string): { tags: string[]; classes: string[]; ids: string[]; attrs: string[][] } {
870
+ const obj: {
871
+ tags: string[];
872
+ classes: string[];
873
+ ids: string[];
874
+ attrs: string[][];
875
+ } = { tags: [], classes: [], ids: [], attrs: [] };
876
+ for (const token of subselector.split(/(?=\.)|(?=#)|(?=\[)/)) {
877
+ switch (token[0]) {
878
+ case '#':
879
+ obj.ids.push(token.slice(1));
880
+ break;
881
+ case '.':
882
+ obj.classes.push(token.slice(1));
883
+ break;
884
+ case '[':
885
+ obj.attrs.push(token.slice(1, -1).split('='));
886
+ break;
887
+ default :
888
+ obj.tags.push(token);
889
+ break;
890
+ }
891
+ }
892
+ return obj;
893
+ }
894
+
895
+ setAsValidAndClearResponse() {
896
+ this.setCustomValidity('');
897
+ this.updateValidClass(true);
898
+ this.clearInvalidResponse();
899
+ }
900
+
901
+ clearInvalidResponse() {
902
+ const $help = this.el.querySelector(this.errorSelector)!;
903
+
904
+ $help.textContent = '';
905
+ }
906
+
907
+ getForm() {
908
+ return this.el.closest(this.options.formSelector || '[uni-form-validate]') as HTMLFormElement;
909
+ }
910
+
911
+ findLabel() {
912
+ const id = this.$input?.id || '';
913
+
914
+ const wrapper = this.$input?.closest('[data-field-wrapper]');
915
+ let label = null;
916
+
917
+ if (wrapper) {
918
+ label = wrapper.querySelector('[data-field-label]');
919
+ }
920
+
921
+ if (!label) {
922
+ label = document.querySelector(`label[for="${id}"]`);
923
+ }
924
+
925
+ return label;
926
+ }
927
+ }
928
+
929
+ function camelTo(str: string, sep: string) {
930
+ return str.replace(/([a-z])([A-Z])/g, `$1${sep}$2`).toLowerCase();
931
+ }
932
+
933
+ validatorHandlers.username = function (value: any, element: HTMLElement) {
934
+ const regex = new RegExp('[\<|\>|"|\'|\%|\;|\(|\)|\&]', 'i');
935
+ return !regex.test(value);
936
+ };
937
+
938
+ validatorHandlers.numeric = function (value: any, element: HTMLElement) {
939
+ const regex = /^(\d|-)?(\d|,)*\.?\d*$/;
940
+ return regex.test(value);
941
+ };
942
+
943
+ validatorHandlers.email = function (value: any, element: HTMLElement) {
944
+ value = punycode.toASCII(value);
945
+ const regex = /^[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
946
+ return regex.test(value);
947
+ };
948
+
949
+ validatorHandlers.url = function (value: any, element: HTMLElement) {
950
+ const regex = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i;
951
+ return regex.test(value);
952
+ };
953
+
954
+ validatorHandlers.alnum = function (value: any, element: HTMLElement) {
955
+ const regex = /^[a-zA-Z0-9]*$/;
956
+ return regex.test(value);
957
+ };
958
+
959
+ validatorHandlers.color = function (value: any, element: HTMLElement) {
960
+ const regex = /^#(?:[0-9a-f]{3}){1,2}$/;
961
+ return regex.test(value);
962
+ };
963
+
964
+ /**
965
+ * @see http://www.virtuosimedia.com/dev/php/37-tested-php-perl-and-javascript-regular-expressions
966
+ */
967
+ validatorHandlers.creditcard = function (value: any, element: HTMLElement) {
968
+ const regex = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|622((12[6-9]|1[3-9][0-9])|([2-8][0-9][0-9])|(9(([0-1][0-9])|(2[0-5]))))[0-9]{10}|64[4-9][0-9]{13}|65[0-9]{14}|3(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})*$/;
969
+ return regex.test(value);
970
+ };
971
+
972
+ validatorHandlers.ip = function (value: any, element: HTMLElement) {
973
+ const regex = /^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))*$/;
974
+ return regex.test(value);
975
+ };
976
+
977
+ validatorHandlers['password-confirm'] = function (value: any, element: HTMLElement) {
978
+ const selector = element.dataset.confirmTarget;
979
+
980
+ if (!selector) {
981
+ throw new Error('Validator: "password-confirm" must add "data-confirm-target" attribute.');
982
+ }
983
+
984
+ const target = document.querySelector<HTMLInputElement>(selector);
985
+
986
+ return target?.value === value;
987
+ };
988
+
989
+ export { validatorHandlers as validators };
990
+
991
+ // customElements.define(UnicornFormValidateElement.is, UnicornFormValidateElement);
992
+ // customElements.define(UnicornFieldValidateElement.is, UnicornFieldValidateElement);
993
+
994
+ export const ready = Promise.all([
995
+ useUniDirective('form-validate', {
996
+ mounted(el, binding) {
997
+ getBoundedInstance(el, 'form.validation', (ele) => {
998
+ return new UnicornFormValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
999
+ });
1000
+ },
1001
+ updated(el, binding) {
1002
+ const instance = getBoundedInstance<UnicornFormValidation>(el, 'form.validation');
1003
+ instance.mergeOptions(JSON.parse(binding.value || '{}'));
1004
+ }
1005
+ }),
1006
+
1007
+ useUniDirective('field-validate', {
1008
+ mounted(el, binding) {
1009
+ getBoundedInstance<UnicornFieldValidation>(el, 'field.validation', (ele) => {
1010
+ return new UnicornFieldValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
1011
+ });
1012
+ },
1013
+
1014
+ updated(el, binding) {
1015
+ const instance = getBoundedInstance<UnicornFieldValidation>(el, 'field.validation');
1016
+ instance.mergeOptions(JSON.parse(binding.value || '{}') || {});
1017
+ }
1018
+ })
1019
+ ]);
1020
+
1021
+ function handleParamValue(value: any) {
1022
+ if (!isNaN(Number(value))) {
1023
+ return Number(value);
1024
+ }
1025
+
1026
+ if (value === 'null') {
1027
+ return null;
1028
+ }
1029
+
1030
+ if (value === 'true') {
1031
+ return true;
1032
+ }
1033
+
1034
+ if (value === 'false') {
1035
+ return true;
1036
+ }
1037
+
1038
+ return value;
1039
+ }
1040
+
1041
+ export interface ValidationModule {
1042
+ UnicornFormValidation: typeof UnicornFormValidation;
1043
+ UnicornFieldValidation: typeof UnicornFieldValidation;
1044
+ ready: Promise<any>;
1045
+ validators: typeof validatorHandlers;
1046
+ }