@windwalker-io/unicorn-next 0.1.0 → 0.1.1

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 (135) 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 +1 -1
  16. package/dist/chunks/form.js.map +1 -1
  17. package/dist/chunks/grid.js.map +1 -1
  18. package/dist/chunks/http-client.js.map +1 -1
  19. package/dist/chunks/iframe-modal.js.map +1 -1
  20. package/dist/chunks/keep-tab.js +1 -1
  21. package/dist/chunks/keep-tab.js.map +1 -1
  22. package/dist/chunks/legacy.js +3 -3
  23. package/dist/chunks/legacy.js.map +1 -1
  24. package/dist/chunks/list-dependent.js.map +1 -1
  25. package/dist/chunks/s3-multipart-uploader.js.map +1 -1
  26. package/dist/chunks/s3-uploader.js.map +1 -1
  27. package/dist/chunks/show-on.js.map +1 -1
  28. package/dist/chunks/tinymce.js +14 -4
  29. package/dist/chunks/tinymce.js.map +1 -1
  30. package/dist/chunks/ui-bootstrap5.js +9 -2
  31. package/dist/chunks/ui-bootstrap5.js.map +1 -1
  32. package/dist/chunks/unicorn.js +94 -46
  33. package/dist/chunks/unicorn.js.map +1 -1
  34. package/dist/chunks/validation.js.map +1 -1
  35. package/dist/editor.css +1 -1
  36. package/dist/index.d.ts +55 -36
  37. package/dist/multi-level-menu.css +1 -1
  38. package/dist/switcher.css +1 -1
  39. package/dist/unicorn.js +22 -22
  40. package/fusionfile.mjs +155 -155
  41. package/package.json +102 -102
  42. package/scss/bootstrap/multi-level-menu.scss +121 -121
  43. package/scss/editor.scss +116 -116
  44. package/scss/field/file-drag.scss +102 -102
  45. package/scss/field/single-image-drag.scss +88 -88
  46. package/scss/field/vue-drag-uploader.scss +160 -160
  47. package/scss/switcher.scss +156 -156
  48. package/src/app.ts +128 -128
  49. package/src/bootstrap/button-radio.ts +208 -208
  50. package/src/bootstrap/keep-tab.ts +155 -155
  51. package/src/composable/index.ts +21 -21
  52. package/src/composable/useCheckboxesMultiSelect.ts +22 -22
  53. package/src/composable/useFieldCascadeSelect.ts +9 -9
  54. package/src/composable/useFieldFileDrag.ts +9 -9
  55. package/src/composable/useFieldFlatpickr.ts +3 -3
  56. package/src/composable/useFieldModalSelect.ts +6 -6
  57. package/src/composable/useFieldModalTree.ts +3 -3
  58. package/src/composable/useFieldMultiUploader.ts +3 -3
  59. package/src/composable/useFieldRepeatable.ts +9 -9
  60. package/src/composable/useFieldSingleImageDrag.ts +5 -5
  61. package/src/composable/useForm.ts +43 -43
  62. package/src/composable/useGrid.ts +57 -57
  63. package/src/composable/useHttp.ts +9 -8
  64. package/src/composable/useIframeModal.ts +9 -9
  65. package/src/composable/useListDependent.ts +26 -26
  66. package/src/composable/useQueue.ts +13 -13
  67. package/src/composable/useS3Uploader.ts +32 -32
  68. package/src/composable/useShowOn.ts +9 -9
  69. package/src/composable/useStack.ts +13 -13
  70. package/src/composable/useTinymce.ts +29 -29
  71. package/src/composable/useTomSelect.ts +72 -72
  72. package/src/composable/useUIBootstrap5.ts +48 -48
  73. package/src/composable/useUniDirective.ts +32 -32
  74. package/src/composable/useValidation.ts +39 -39
  75. package/src/data.ts +34 -36
  76. package/src/events.ts +82 -73
  77. package/src/legacy/legacy.ts +186 -186
  78. package/src/legacy/loader.ts +125 -125
  79. package/src/module/checkboxes-multi-select.ts +54 -54
  80. package/src/module/field-cascade-select.ts +292 -292
  81. package/src/module/field-file-drag.ts +292 -292
  82. package/src/module/field-flatpickr.ts +127 -127
  83. package/src/module/field-modal-select.ts +174 -174
  84. package/src/module/field-modal-tree.ts +27 -27
  85. package/src/module/field-multi-uploader.ts +361 -361
  86. package/src/module/field-repeatable.ts +202 -202
  87. package/src/module/field-single-image-drag.ts +468 -468
  88. package/src/module/form.ts +223 -223
  89. package/src/module/grid.ts +465 -465
  90. package/src/module/http-client.ts +248 -243
  91. package/src/module/iframe-modal.ts +167 -167
  92. package/src/module/list-dependent.ts +321 -321
  93. package/src/module/s3-multipart-uploader.ts +300 -300
  94. package/src/module/s3-uploader.ts +234 -234
  95. package/src/module/show-on.ts +173 -173
  96. package/src/module/tinymce.ts +279 -263
  97. package/src/module/ui-bootstrap5.ts +116 -107
  98. package/src/module/validation.ts +1019 -1019
  99. package/src/plugin/index.ts +1 -1
  100. package/src/plugin/php-adapter.ts +65 -65
  101. package/src/polyfill/form-request-submit.ts +31 -31
  102. package/src/polyfill/index.ts +9 -9
  103. package/src/service/animate.ts +58 -58
  104. package/src/service/crypto.ts +27 -27
  105. package/src/service/dom-watcher.ts +62 -62
  106. package/src/service/dom.ts +265 -265
  107. package/src/service/helper.ts +48 -48
  108. package/src/service/index.ts +10 -10
  109. package/src/service/lang.ts +122 -122
  110. package/src/service/loader.ts +152 -152
  111. package/src/service/router.ts +118 -118
  112. package/src/service/ui.ts +525 -497
  113. package/src/service/uri.ts +106 -106
  114. package/src/types/base.ts +9 -9
  115. package/src/types/index.ts +4 -4
  116. package/src/types/modal-tree.ts +12 -12
  117. package/src/types/plugin.ts +6 -6
  118. package/src/types/shims.d.ts +18 -18
  119. package/src/types/ui.ts +6 -6
  120. package/src/unicorn.ts +79 -63
  121. package/src/utilities/arr.ts +25 -25
  122. package/src/utilities/base.ts +9 -9
  123. package/src/utilities/data.ts +48 -48
  124. package/src/utilities/index.ts +5 -5
  125. package/src/utilities/tree.ts +20 -20
  126. package/src/vue/components/ModalTree/ModalTreeApp.vue +175 -175
  127. package/src/vue/components/ModalTree/TreeItem.vue +262 -262
  128. package/src/vue/components/ModalTree/TreeModal.vue +225 -225
  129. package/tests/test.js +4 -4
  130. package/tsconfig.js.json +25 -25
  131. package/tsconfig.json +17 -17
  132. package/vite.assets.config.ts +61 -61
  133. package/vite.config.test.ts +36 -36
  134. package/vite.config.ts +112 -112
  135. package/dist/unicorn-next.css +0 -12
@@ -1,1019 +1,1019 @@
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
- $input.validationMessage = '';
406
- $input.setCustomValidity = (msg: string) => {
407
- $input.validationMessage = String(msg);
408
- };
409
-
410
- $input.checkValidity = () => {
411
- return this.checkInputOptionsValidity();
412
- };
413
- }
414
- }
415
-
416
- bindEvents() {
417
- if (!this.$input) {
418
- return;
419
- }
420
-
421
- this.$input.addEventListener('invalid', (e) => {
422
- this.showInvalidResponse();
423
- });
424
-
425
- const events = this.options.events;
426
-
427
- events.forEach((eventName) => {
428
- this.$input?.addEventListener(eventName, () => {
429
- this.checkValidity();
430
- });
431
- });
432
- }
433
-
434
- prepareWrapper() {
435
- if (this.el.querySelector(this.errorSelector)?.classList?.contains('invalid-tooltip')) {
436
- if (window.getComputedStyle(this.el).position === 'static') {
437
- this.el.style.position = 'relative';
438
- }
439
- }
440
- }
441
-
442
- checkValidity() {
443
- if (!this.$input) {
444
- return true;
445
- }
446
-
447
- if (this.$input.hasAttribute('readonly')) {
448
- return true;
449
- }
450
-
451
- if (this.$input.hasAttribute('[data-novalidate]')) {
452
- return true;
453
- }
454
-
455
- if (this.$input.closest('[data-novalidate]')) {
456
- return true;
457
- }
458
-
459
- this.$input.setCustomValidity('');
460
- let valid = this.$input.checkValidity();
461
-
462
- if (valid && this.$form) {
463
- valid = this.runCustomValidity();
464
- }
465
-
466
- // Raise invalid event
467
- // this.$input.checkValidity();
468
-
469
- this.updateValidClass(valid);
470
-
471
- return valid;
472
- }
473
-
474
- runCustomValidity() {
475
- if (!this.$input) {
476
- return true;
477
- }
478
-
479
- // Check custom validity
480
- const validates = (this.$input.getAttribute('data-validate') || '').split('|');
481
- let result = true;
482
-
483
- if (this.$input.value !== '' && validates.length) {
484
- if (!this.checkCustomDataAttributeValidity()) {
485
- return false;
486
- }
487
-
488
- for (const validatorName of validates) {
489
- const [validator, options] = this.getValidator(validatorName) || [null, {}];
490
-
491
- if (!validator) {
492
- continue;
493
- }
494
-
495
- Object.assign(options, validator.options);
496
-
497
- let r = validator.handler(this.$input.value, this.$input, options, this);
498
-
499
- // If return is a promise, push to stack and resolve later
500
- if (r instanceof Promise || (typeof r === 'object' && r.then)) {
501
- r.then((result: boolean) => {
502
- this.handleAsyncCustomResult(result, validator);
503
- });
504
- continue;
505
- }
506
-
507
- if (!this.handleCustomResult(r, validator)) {
508
- result = false;
509
-
510
- break;
511
- }
512
- }
513
- }
514
-
515
- return result;
516
- }
517
-
518
- async checkValidityAsync() {
519
- if (!this.$input) {
520
- return true;
521
- }
522
-
523
- if (this.$input.hasAttribute('readonly')) {
524
- return true;
525
- }
526
-
527
- this.$input.setCustomValidity('');
528
- let valid = this.$input.checkValidity();
529
-
530
- if (valid && this.$form) {
531
- valid = await this.runCustomValidityAsync();
532
- }
533
-
534
- this.updateValidClass(valid);
535
-
536
- return valid;
537
- }
538
-
539
- async runCustomValidityAsync(): Promise<boolean> {
540
- if (!this.$input) {
541
- return true;
542
- }
543
-
544
- // Check custom validity
545
- const validates = (this.$input.getAttribute('data-validate') || '').split('|');
546
-
547
- const results: Array<boolean | string | undefined> = [];
548
- const promises: Promise<boolean>[] = [];
549
-
550
- if (this.$input.value !== '' && validates.length) {
551
- if (!this.checkCustomDataAttributeValidity()) {
552
- return false;
553
- }
554
-
555
- for (const validatorName of validates) {
556
- let [validator, options] = this.getValidator(validatorName) || [null, {}];
557
-
558
- if (!validator) {
559
- continue;
560
- }
561
-
562
- options = Object.assign({}, options, validator.options || {});
563
-
564
- promises.push(
565
- Promise.resolve(validator.handler(this.$input.value, this.$input, options, this))
566
- .then((r) => {
567
- results.push(this.handleAsyncCustomResult(r, validator));
568
-
569
- return r;
570
- })
571
- );
572
- }
573
- }
574
-
575
- await Promise.all(promises);
576
-
577
- for (const result of results) {
578
- if (result === false) {
579
- return false;
580
- }
581
- }
582
-
583
- return true;
584
- }
585
-
586
- checkCustomDataAttributeValidity(): boolean {
587
- const error = this.$input?.dataset.validationFail;
588
-
589
- return this.handleCustomResult(error);
590
- }
591
-
592
- checkInputOptionsValidity(): boolean {
593
- if (!this.$input) {
594
- return true;
595
- }
596
-
597
- const isRequired = this.$input.getAttribute('required') != null;
598
- const optionWrappers = this.$input.querySelectorAll(this.options.inputOptionsSelector);
599
- let result = true;
600
-
601
- if (isRequired) {
602
- for (const optionWrapper of optionWrappers) {
603
- const input = optionWrapper.querySelector('input');
604
-
605
- result = false;
606
-
607
- // Only need one checked
608
- if (input?.checked) {
609
- result = true;
610
- break;
611
- }
612
- }
613
- }
614
-
615
- // Get browser input validation message
616
- const n = document.createElement('input');
617
- n.required = isRequired;
618
-
619
- if (result) {
620
- n.value = 'placeholder';
621
- }
622
-
623
- n.checkValidity();
624
-
625
- (this.$input as any).validationMessage = n.validationMessage;
626
- (this.$input as any).validity = n.validity;
627
-
628
- for (const optionWrapper of optionWrappers) {
629
- const input = optionWrapper.querySelector<HTMLInputElement>('input');
630
-
631
- input?.setCustomValidity(n.validationMessage);
632
- }
633
-
634
- if (!result) {
635
- this.$input.dispatchEvent(
636
- new CustomEvent('invalid')
637
- );
638
- }
639
-
640
- return result;
641
- }
642
-
643
- /**
644
- * @param valid {boolean}
645
- */
646
- updateValidClass(valid: Boolean) {
647
- const $errorElement = this.getErrorElement();
648
- const $invalidTarget = $errorElement?.previousElementSibling;
649
-
650
- this.$input?.classList.remove(this.invalidClass);
651
- this.$input?.classList.remove(this.validClass);
652
- this.el.classList.remove(this.invalidClass);
653
- this.el.classList.remove(this.validClass);
654
- $invalidTarget?.classList.remove(this.invalidClass);
655
- $invalidTarget?.classList.remove(this.validClass);
656
-
657
- if (valid) {
658
- this.$input?.classList.add(this.validClass);
659
- this.el.classList.add(this.validClass);
660
-
661
- $invalidTarget?.classList.add(this.validClass);
662
- } else {
663
- this.$input?.classList.add(this.invalidClass);
664
- this.el.classList.add(this.invalidClass);
665
-
666
- $invalidTarget?.classList.add(this.invalidClass);
667
- }
668
- }
669
-
670
- getFormValidation(element?: Nullable<HTMLFormElement>): UnicornFormValidation | null {
671
- return getBoundedInstance(element || this.getForm(), 'form.validation')!;
672
- }
673
-
674
- getValidator(name: string): [Validator, Record<string, any>] | null {
675
- const matches = name.match(/(?<type>[\w\-_]+)(\((?<params>.*)\))*/);
676
-
677
- if (!matches) {
678
- return null;
679
- }
680
-
681
- const validatorName = matches.groups?.type || '';
682
-
683
- const params = matches.groups?.params || '';
684
-
685
- const fv = this.getFormValidation(this.$form!);
686
- const validator = fv?.validators[validatorName] || UnicornFormValidation.globalValidators[validatorName];
687
-
688
- if (!validator) {
689
- return null;
690
- }
691
-
692
- const paramMatches = params.matchAll(/(?<key>\w+)(\s?[=:]\s?(?<value>\w+))?/g);
693
- const options: Record<string, string> = {};
694
-
695
- for (const paramMatch of paramMatches) {
696
- const match = paramMatch?.groups as {
697
- key: string;
698
- value: string;
699
- } | undefined;
700
-
701
- if (!match) {
702
- continue;
703
- }
704
-
705
- options[match.key] = handleParamValue(match.value);
706
- }
707
-
708
- return [ validator, options ];
709
- }
710
-
711
- handleCustomResult(result: boolean | string | undefined, validator?: Nullable<Validator>): boolean {
712
- if (typeof result === 'string') {
713
- this.$input?.setCustomValidity(result);
714
- result = result === '';
715
- } else if (result === undefined) {
716
- result = true;
717
- }
718
-
719
- if (result) {
720
- this.$input?.setCustomValidity('');
721
- } else if (validator) {
722
- this.raiseCustomErrorState(validator);
723
- }
724
-
725
- return result;
726
- }
727
-
728
- handleAsyncCustomResult(result: boolean, validator?: Nullable<Validator>): boolean {
729
- result = this.handleCustomResult(result, validator);
730
-
731
- // Fire invalid events
732
- this.$input?.checkValidity();
733
-
734
- this.updateValidClass(result);
735
-
736
- return result;
737
- }
738
-
739
- raiseCustomErrorState(validator: Validator): void {
740
- let help;
741
-
742
- if (this.$input?.validationMessage === '') {
743
- help = validator.options?.notice;
744
-
745
- if (typeof help === 'function') {
746
- help = help(this.$input, this);
747
- }
748
-
749
- if (help != null) {
750
- this.$input?.setCustomValidity(help);
751
- }
752
- }
753
-
754
- if (this.$input?.validationMessage === '') {
755
- this.$input?.setCustomValidity(trans('unicorn.message.validation.custom.error'));
756
- }
757
-
758
- this.$input?.dispatchEvent(
759
- new CustomEvent('invalid')
760
- );
761
- }
762
-
763
- setAsInvalidAndReport(error: string) {
764
- this.setCustomValidity(error);
765
- this.showInvalidResponse();
766
- }
767
-
768
- setCustomValidity(error: string) {
769
- this.$input?.setCustomValidity(error);
770
- }
771
-
772
- reportValidity() {
773
- if (this.validationMessage !== '') {
774
- this.showInvalidResponse();
775
- }
776
- }
777
-
778
- showInvalidResponse() {
779
- /** @type ValidityState */
780
- const state = this.$input?.validity;
781
- let message: string = this.$input?.validationMessage || '';
782
-
783
- for (let key in state) {
784
- if (state[(key as keyof ValidityState)] && this.$input?.dataset[key + 'Message']) {
785
- message = this.$input?.dataset[key + 'Message'] || '';
786
- break;
787
- }
788
- }
789
-
790
- if (!this.isVisible) {
791
- let title = this.findLabel()?.textContent;
792
-
793
- if (!title) {
794
- title = this.$input?.name || '';
795
- }
796
-
797
- useUITheme().renderMessage(
798
- `Field: ${title} - ${message}`,
799
- 'warning'
800
- );
801
- }
802
-
803
- let $help = this.getErrorElement();
804
-
805
- if (!$help) {
806
- $help = this.createHelpElement()!;
807
- this.el.appendChild($help);
808
- this.prepareWrapper();
809
- }
810
-
811
- $help.textContent = message;
812
-
813
- this.updateValidClass(false);
814
- }
815
-
816
- getErrorElement() {
817
- return this.el.querySelector(this.errorSelector);
818
- }
819
-
820
- createHelpElement() {
821
- const className = this.options.errorMessageClass;
822
- const parsed = this.parseSelector(this.errorSelector || '');
823
-
824
- const $help = html(`<div class="${className}"></div>`)!;
825
-
826
- $help.classList.add(...parsed.classes);
827
-
828
- parsed.attrs.forEach((attr) => {
829
- $help.setAttribute(attr[0], attr[1] || '');
830
- });
831
-
832
- parsed.ids.forEach((id) => {
833
- $help.id = id;
834
- });
835
-
836
- return $help;
837
- }
838
-
839
- /**
840
- * @see https://stackoverflow.com/a/17888178
841
- */
842
- parseSelector(subselector: string): { tags: string[]; classes: string[]; ids: string[]; attrs: string[][] } {
843
- const obj: {
844
- tags: string[];
845
- classes: string[];
846
- ids: string[];
847
- attrs: string[][];
848
- } = { tags: [], classes: [], ids: [], attrs: [] };
849
- for (const token of subselector.split(/(?=\.)|(?=#)|(?=\[)/)) {
850
- switch (token[0]) {
851
- case '#':
852
- obj.ids.push(token.slice(1));
853
- break;
854
- case '.':
855
- obj.classes.push(token.slice(1));
856
- break;
857
- case '[':
858
- obj.attrs.push(token.slice(1, -1).split('='));
859
- break;
860
- default :
861
- obj.tags.push(token);
862
- break;
863
- }
864
- }
865
- return obj;
866
- }
867
-
868
- setAsValidAndClearResponse() {
869
- this.setCustomValidity('');
870
- this.updateValidClass(true);
871
- this.clearInvalidResponse();
872
- }
873
-
874
- clearInvalidResponse() {
875
- const $help = this.el.querySelector(this.errorSelector)!;
876
-
877
- $help.textContent = '';
878
- }
879
-
880
- getForm() {
881
- return this.el.closest(this.options.formSelector || '[uni-form-validate]') as HTMLFormElement;
882
- }
883
-
884
- findLabel() {
885
- const id = this.$input?.id || '';
886
-
887
- const wrapper = this.$input?.closest('[data-field-wrapper]');
888
- let label = null;
889
-
890
- if (wrapper) {
891
- label = wrapper.querySelector('[data-field-label]');
892
- }
893
-
894
- if (!label) {
895
- label = document.querySelector(`label[for="${id}"]`);
896
- }
897
-
898
- return label;
899
- }
900
- }
901
-
902
- function camelTo(str: string, sep: string) {
903
- return str.replace(/([a-z])([A-Z])/g, `$1${sep}$2`).toLowerCase();
904
- }
905
-
906
- validatorHandlers.username = function (value: any, element: HTMLElement) {
907
- const regex = new RegExp('[\<|\>|"|\'|\%|\;|\(|\)|\&]', 'i');
908
- return !regex.test(value);
909
- };
910
-
911
- validatorHandlers.numeric = function (value: any, element: HTMLElement) {
912
- const regex = /^(\d|-)?(\d|,)*\.?\d*$/;
913
- return regex.test(value);
914
- };
915
-
916
- validatorHandlers.email = function (value: any, element: HTMLElement) {
917
- value = punycode.toASCII(value);
918
- const regex = /^[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
919
- return regex.test(value);
920
- };
921
-
922
- validatorHandlers.url = function (value: any, element: HTMLElement) {
923
- 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;
924
- return regex.test(value);
925
- };
926
-
927
- validatorHandlers.alnum = function (value: any, element: HTMLElement) {
928
- const regex = /^[a-zA-Z0-9]*$/;
929
- return regex.test(value);
930
- };
931
-
932
- validatorHandlers.color = function (value: any, element: HTMLElement) {
933
- const regex = /^#(?:[0-9a-f]{3}){1,2}$/;
934
- return regex.test(value);
935
- };
936
-
937
- /**
938
- * @see http://www.virtuosimedia.com/dev/php/37-tested-php-perl-and-javascript-regular-expressions
939
- */
940
- validatorHandlers.creditcard = function (value: any, element: HTMLElement) {
941
- 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})*$/;
942
- return regex.test(value);
943
- };
944
-
945
- validatorHandlers.ip = function (value: any, element: HTMLElement) {
946
- 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]?))*$/;
947
- return regex.test(value);
948
- };
949
-
950
- validatorHandlers['password-confirm'] = function (value: any, element: HTMLElement) {
951
- const selector = element.dataset.confirmTarget;
952
-
953
- if (!selector) {
954
- throw new Error('Validator: "password-confirm" must add "data-confirm-target" attribute.');
955
- }
956
-
957
- const target = document.querySelector<HTMLInputElement>(selector);
958
-
959
- return target?.value === value;
960
- };
961
-
962
- export { validatorHandlers as validators };
963
-
964
- // customElements.define(UnicornFormValidateElement.is, UnicornFormValidateElement);
965
- // customElements.define(UnicornFieldValidateElement.is, UnicornFieldValidateElement);
966
-
967
- export const ready = Promise.all([
968
- useUniDirective('form-validate', {
969
- mounted(el, binding) {
970
- getBoundedInstance(el, 'form.validation', (ele) => {
971
- return new UnicornFormValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
972
- });
973
- },
974
- updated(el, binding) {
975
- const instance = getBoundedInstance<UnicornFormValidation>(el, 'form.validation');
976
- instance.mergeOptions(JSON.parse(binding.value || '{}'));
977
- }
978
- }),
979
-
980
- useUniDirective('field-validate', {
981
- mounted(el, binding) {
982
- getBoundedInstance<UnicornFieldValidation>(el, 'field.validation', (ele) => {
983
- return new UnicornFieldValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
984
- });
985
- },
986
-
987
- updated(el, binding) {
988
- const instance = getBoundedInstance<UnicornFieldValidation>(el, 'field.validation');
989
- instance.mergeOptions(JSON.parse(binding.value || '{}') || {});
990
- }
991
- })
992
- ]);
993
-
994
- function handleParamValue(value: any) {
995
- if (!isNaN(Number(value))) {
996
- return Number(value);
997
- }
998
-
999
- if (value === 'null') {
1000
- return null;
1001
- }
1002
-
1003
- if (value === 'true') {
1004
- return true;
1005
- }
1006
-
1007
- if (value === 'false') {
1008
- return true;
1009
- }
1010
-
1011
- return value;
1012
- }
1013
-
1014
- export interface ValidationModule {
1015
- UnicornFormValidation: typeof UnicornFormValidation;
1016
- UnicornFieldValidation: typeof UnicornFieldValidation;
1017
- ready: Promise<any>;
1018
- validators: typeof validatorHandlers;
1019
- }
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
+ $input.validationMessage = '';
406
+ $input.setCustomValidity = (msg: string) => {
407
+ $input.validationMessage = String(msg);
408
+ };
409
+
410
+ $input.checkValidity = () => {
411
+ return this.checkInputOptionsValidity();
412
+ };
413
+ }
414
+ }
415
+
416
+ bindEvents() {
417
+ if (!this.$input) {
418
+ return;
419
+ }
420
+
421
+ this.$input.addEventListener('invalid', (e) => {
422
+ this.showInvalidResponse();
423
+ });
424
+
425
+ const events = this.options.events;
426
+
427
+ events.forEach((eventName) => {
428
+ this.$input?.addEventListener(eventName, () => {
429
+ this.checkValidity();
430
+ });
431
+ });
432
+ }
433
+
434
+ prepareWrapper() {
435
+ if (this.el.querySelector(this.errorSelector)?.classList?.contains('invalid-tooltip')) {
436
+ if (window.getComputedStyle(this.el).position === 'static') {
437
+ this.el.style.position = 'relative';
438
+ }
439
+ }
440
+ }
441
+
442
+ checkValidity() {
443
+ if (!this.$input) {
444
+ return true;
445
+ }
446
+
447
+ if (this.$input.hasAttribute('readonly')) {
448
+ return true;
449
+ }
450
+
451
+ if (this.$input.hasAttribute('[data-novalidate]')) {
452
+ return true;
453
+ }
454
+
455
+ if (this.$input.closest('[data-novalidate]')) {
456
+ return true;
457
+ }
458
+
459
+ this.$input.setCustomValidity('');
460
+ let valid = this.$input.checkValidity();
461
+
462
+ if (valid && this.$form) {
463
+ valid = this.runCustomValidity();
464
+ }
465
+
466
+ // Raise invalid event
467
+ // this.$input.checkValidity();
468
+
469
+ this.updateValidClass(valid);
470
+
471
+ return valid;
472
+ }
473
+
474
+ runCustomValidity() {
475
+ if (!this.$input) {
476
+ return true;
477
+ }
478
+
479
+ // Check custom validity
480
+ const validates = (this.$input.getAttribute('data-validate') || '').split('|');
481
+ let result = true;
482
+
483
+ if (this.$input.value !== '' && validates.length) {
484
+ if (!this.checkCustomDataAttributeValidity()) {
485
+ return false;
486
+ }
487
+
488
+ for (const validatorName of validates) {
489
+ const [validator, options] = this.getValidator(validatorName) || [null, {}];
490
+
491
+ if (!validator) {
492
+ continue;
493
+ }
494
+
495
+ Object.assign(options, validator.options);
496
+
497
+ let r = validator.handler(this.$input.value, this.$input, options, this);
498
+
499
+ // If return is a promise, push to stack and resolve later
500
+ if (r instanceof Promise || (typeof r === 'object' && r.then)) {
501
+ r.then((result: boolean) => {
502
+ this.handleAsyncCustomResult(result, validator);
503
+ });
504
+ continue;
505
+ }
506
+
507
+ if (!this.handleCustomResult(r, validator)) {
508
+ result = false;
509
+
510
+ break;
511
+ }
512
+ }
513
+ }
514
+
515
+ return result;
516
+ }
517
+
518
+ async checkValidityAsync() {
519
+ if (!this.$input) {
520
+ return true;
521
+ }
522
+
523
+ if (this.$input.hasAttribute('readonly')) {
524
+ return true;
525
+ }
526
+
527
+ this.$input.setCustomValidity('');
528
+ let valid = this.$input.checkValidity();
529
+
530
+ if (valid && this.$form) {
531
+ valid = await this.runCustomValidityAsync();
532
+ }
533
+
534
+ this.updateValidClass(valid);
535
+
536
+ return valid;
537
+ }
538
+
539
+ async runCustomValidityAsync(): Promise<boolean> {
540
+ if (!this.$input) {
541
+ return true;
542
+ }
543
+
544
+ // Check custom validity
545
+ const validates = (this.$input.getAttribute('data-validate') || '').split('|');
546
+
547
+ const results: Array<boolean | string | undefined> = [];
548
+ const promises: Promise<boolean>[] = [];
549
+
550
+ if (this.$input.value !== '' && validates.length) {
551
+ if (!this.checkCustomDataAttributeValidity()) {
552
+ return false;
553
+ }
554
+
555
+ for (const validatorName of validates) {
556
+ let [validator, options] = this.getValidator(validatorName) || [null, {}];
557
+
558
+ if (!validator) {
559
+ continue;
560
+ }
561
+
562
+ options = Object.assign({}, options, validator.options || {});
563
+
564
+ promises.push(
565
+ Promise.resolve(validator.handler(this.$input.value, this.$input, options, this))
566
+ .then((r) => {
567
+ results.push(this.handleAsyncCustomResult(r, validator));
568
+
569
+ return r;
570
+ })
571
+ );
572
+ }
573
+ }
574
+
575
+ await Promise.all(promises);
576
+
577
+ for (const result of results) {
578
+ if (result === false) {
579
+ return false;
580
+ }
581
+ }
582
+
583
+ return true;
584
+ }
585
+
586
+ checkCustomDataAttributeValidity(): boolean {
587
+ const error = this.$input?.dataset.validationFail;
588
+
589
+ return this.handleCustomResult(error);
590
+ }
591
+
592
+ checkInputOptionsValidity(): boolean {
593
+ if (!this.$input) {
594
+ return true;
595
+ }
596
+
597
+ const isRequired = this.$input.getAttribute('required') != null;
598
+ const optionWrappers = this.$input.querySelectorAll(this.options.inputOptionsSelector);
599
+ let result = true;
600
+
601
+ if (isRequired) {
602
+ for (const optionWrapper of optionWrappers) {
603
+ const input = optionWrapper.querySelector('input');
604
+
605
+ result = false;
606
+
607
+ // Only need one checked
608
+ if (input?.checked) {
609
+ result = true;
610
+ break;
611
+ }
612
+ }
613
+ }
614
+
615
+ // Get browser input validation message
616
+ const n = document.createElement('input');
617
+ n.required = isRequired;
618
+
619
+ if (result) {
620
+ n.value = 'placeholder';
621
+ }
622
+
623
+ n.checkValidity();
624
+
625
+ (this.$input as any).validationMessage = n.validationMessage;
626
+ (this.$input as any).validity = n.validity;
627
+
628
+ for (const optionWrapper of optionWrappers) {
629
+ const input = optionWrapper.querySelector<HTMLInputElement>('input');
630
+
631
+ input?.setCustomValidity(n.validationMessage);
632
+ }
633
+
634
+ if (!result) {
635
+ this.$input.dispatchEvent(
636
+ new CustomEvent('invalid')
637
+ );
638
+ }
639
+
640
+ return result;
641
+ }
642
+
643
+ /**
644
+ * @param valid {boolean}
645
+ */
646
+ updateValidClass(valid: Boolean) {
647
+ const $errorElement = this.getErrorElement();
648
+ const $invalidTarget = $errorElement?.previousElementSibling;
649
+
650
+ this.$input?.classList.remove(this.invalidClass);
651
+ this.$input?.classList.remove(this.validClass);
652
+ this.el.classList.remove(this.invalidClass);
653
+ this.el.classList.remove(this.validClass);
654
+ $invalidTarget?.classList.remove(this.invalidClass);
655
+ $invalidTarget?.classList.remove(this.validClass);
656
+
657
+ if (valid) {
658
+ this.$input?.classList.add(this.validClass);
659
+ this.el.classList.add(this.validClass);
660
+
661
+ $invalidTarget?.classList.add(this.validClass);
662
+ } else {
663
+ this.$input?.classList.add(this.invalidClass);
664
+ this.el.classList.add(this.invalidClass);
665
+
666
+ $invalidTarget?.classList.add(this.invalidClass);
667
+ }
668
+ }
669
+
670
+ getFormValidation(element?: Nullable<HTMLFormElement>): UnicornFormValidation | null {
671
+ return getBoundedInstance(element || this.getForm(), 'form.validation')!;
672
+ }
673
+
674
+ getValidator(name: string): [Validator, Record<string, any>] | null {
675
+ const matches = name.match(/(?<type>[\w\-_]+)(\((?<params>.*)\))*/);
676
+
677
+ if (!matches) {
678
+ return null;
679
+ }
680
+
681
+ const validatorName = matches.groups?.type || '';
682
+
683
+ const params = matches.groups?.params || '';
684
+
685
+ const fv = this.getFormValidation(this.$form!);
686
+ const validator = fv?.validators[validatorName] || UnicornFormValidation.globalValidators[validatorName];
687
+
688
+ if (!validator) {
689
+ return null;
690
+ }
691
+
692
+ const paramMatches = params.matchAll(/(?<key>\w+)(\s?[=:]\s?(?<value>\w+))?/g);
693
+ const options: Record<string, string> = {};
694
+
695
+ for (const paramMatch of paramMatches) {
696
+ const match = paramMatch?.groups as {
697
+ key: string;
698
+ value: string;
699
+ } | undefined;
700
+
701
+ if (!match) {
702
+ continue;
703
+ }
704
+
705
+ options[match.key] = handleParamValue(match.value);
706
+ }
707
+
708
+ return [ validator, options ];
709
+ }
710
+
711
+ handleCustomResult(result: boolean | string | undefined, validator?: Nullable<Validator>): boolean {
712
+ if (typeof result === 'string') {
713
+ this.$input?.setCustomValidity(result);
714
+ result = result === '';
715
+ } else if (result === undefined) {
716
+ result = true;
717
+ }
718
+
719
+ if (result) {
720
+ this.$input?.setCustomValidity('');
721
+ } else if (validator) {
722
+ this.raiseCustomErrorState(validator);
723
+ }
724
+
725
+ return result;
726
+ }
727
+
728
+ handleAsyncCustomResult(result: boolean, validator?: Nullable<Validator>): boolean {
729
+ result = this.handleCustomResult(result, validator);
730
+
731
+ // Fire invalid events
732
+ this.$input?.checkValidity();
733
+
734
+ this.updateValidClass(result);
735
+
736
+ return result;
737
+ }
738
+
739
+ raiseCustomErrorState(validator: Validator): void {
740
+ let help;
741
+
742
+ if (this.$input?.validationMessage === '') {
743
+ help = validator.options?.notice;
744
+
745
+ if (typeof help === 'function') {
746
+ help = help(this.$input, this);
747
+ }
748
+
749
+ if (help != null) {
750
+ this.$input?.setCustomValidity(help);
751
+ }
752
+ }
753
+
754
+ if (this.$input?.validationMessage === '') {
755
+ this.$input?.setCustomValidity(trans('unicorn.message.validation.custom.error'));
756
+ }
757
+
758
+ this.$input?.dispatchEvent(
759
+ new CustomEvent('invalid')
760
+ );
761
+ }
762
+
763
+ setAsInvalidAndReport(error: string) {
764
+ this.setCustomValidity(error);
765
+ this.showInvalidResponse();
766
+ }
767
+
768
+ setCustomValidity(error: string) {
769
+ this.$input?.setCustomValidity(error);
770
+ }
771
+
772
+ reportValidity() {
773
+ if (this.validationMessage !== '') {
774
+ this.showInvalidResponse();
775
+ }
776
+ }
777
+
778
+ showInvalidResponse() {
779
+ /** @type ValidityState */
780
+ const state = this.$input?.validity;
781
+ let message: string = this.$input?.validationMessage || '';
782
+
783
+ for (let key in state) {
784
+ if (state[(key as keyof ValidityState)] && this.$input?.dataset[key + 'Message']) {
785
+ message = this.$input?.dataset[key + 'Message'] || '';
786
+ break;
787
+ }
788
+ }
789
+
790
+ if (!this.isVisible) {
791
+ let title = this.findLabel()?.textContent;
792
+
793
+ if (!title) {
794
+ title = this.$input?.name || '';
795
+ }
796
+
797
+ useUITheme().renderMessage(
798
+ `Field: ${title} - ${message}`,
799
+ 'warning'
800
+ );
801
+ }
802
+
803
+ let $help = this.getErrorElement();
804
+
805
+ if (!$help) {
806
+ $help = this.createHelpElement()!;
807
+ this.el.appendChild($help);
808
+ this.prepareWrapper();
809
+ }
810
+
811
+ $help.textContent = message;
812
+
813
+ this.updateValidClass(false);
814
+ }
815
+
816
+ getErrorElement() {
817
+ return this.el.querySelector(this.errorSelector);
818
+ }
819
+
820
+ createHelpElement() {
821
+ const className = this.options.errorMessageClass;
822
+ const parsed = this.parseSelector(this.errorSelector || '');
823
+
824
+ const $help = html(`<div class="${className}"></div>`)!;
825
+
826
+ $help.classList.add(...parsed.classes);
827
+
828
+ parsed.attrs.forEach((attr) => {
829
+ $help.setAttribute(attr[0], attr[1] || '');
830
+ });
831
+
832
+ parsed.ids.forEach((id) => {
833
+ $help.id = id;
834
+ });
835
+
836
+ return $help;
837
+ }
838
+
839
+ /**
840
+ * @see https://stackoverflow.com/a/17888178
841
+ */
842
+ parseSelector(subselector: string): { tags: string[]; classes: string[]; ids: string[]; attrs: string[][] } {
843
+ const obj: {
844
+ tags: string[];
845
+ classes: string[];
846
+ ids: string[];
847
+ attrs: string[][];
848
+ } = { tags: [], classes: [], ids: [], attrs: [] };
849
+ for (const token of subselector.split(/(?=\.)|(?=#)|(?=\[)/)) {
850
+ switch (token[0]) {
851
+ case '#':
852
+ obj.ids.push(token.slice(1));
853
+ break;
854
+ case '.':
855
+ obj.classes.push(token.slice(1));
856
+ break;
857
+ case '[':
858
+ obj.attrs.push(token.slice(1, -1).split('='));
859
+ break;
860
+ default :
861
+ obj.tags.push(token);
862
+ break;
863
+ }
864
+ }
865
+ return obj;
866
+ }
867
+
868
+ setAsValidAndClearResponse() {
869
+ this.setCustomValidity('');
870
+ this.updateValidClass(true);
871
+ this.clearInvalidResponse();
872
+ }
873
+
874
+ clearInvalidResponse() {
875
+ const $help = this.el.querySelector(this.errorSelector)!;
876
+
877
+ $help.textContent = '';
878
+ }
879
+
880
+ getForm() {
881
+ return this.el.closest(this.options.formSelector || '[uni-form-validate]') as HTMLFormElement;
882
+ }
883
+
884
+ findLabel() {
885
+ const id = this.$input?.id || '';
886
+
887
+ const wrapper = this.$input?.closest('[data-field-wrapper]');
888
+ let label = null;
889
+
890
+ if (wrapper) {
891
+ label = wrapper.querySelector('[data-field-label]');
892
+ }
893
+
894
+ if (!label) {
895
+ label = document.querySelector(`label[for="${id}"]`);
896
+ }
897
+
898
+ return label;
899
+ }
900
+ }
901
+
902
+ function camelTo(str: string, sep: string) {
903
+ return str.replace(/([a-z])([A-Z])/g, `$1${sep}$2`).toLowerCase();
904
+ }
905
+
906
+ validatorHandlers.username = function (value: any, element: HTMLElement) {
907
+ const regex = new RegExp('[\<|\>|"|\'|\%|\;|\(|\)|\&]', 'i');
908
+ return !regex.test(value);
909
+ };
910
+
911
+ validatorHandlers.numeric = function (value: any, element: HTMLElement) {
912
+ const regex = /^(\d|-)?(\d|,)*\.?\d*$/;
913
+ return regex.test(value);
914
+ };
915
+
916
+ validatorHandlers.email = function (value: any, element: HTMLElement) {
917
+ value = punycode.toASCII(value);
918
+ const regex = /^[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
919
+ return regex.test(value);
920
+ };
921
+
922
+ validatorHandlers.url = function (value: any, element: HTMLElement) {
923
+ 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;
924
+ return regex.test(value);
925
+ };
926
+
927
+ validatorHandlers.alnum = function (value: any, element: HTMLElement) {
928
+ const regex = /^[a-zA-Z0-9]*$/;
929
+ return regex.test(value);
930
+ };
931
+
932
+ validatorHandlers.color = function (value: any, element: HTMLElement) {
933
+ const regex = /^#(?:[0-9a-f]{3}){1,2}$/;
934
+ return regex.test(value);
935
+ };
936
+
937
+ /**
938
+ * @see http://www.virtuosimedia.com/dev/php/37-tested-php-perl-and-javascript-regular-expressions
939
+ */
940
+ validatorHandlers.creditcard = function (value: any, element: HTMLElement) {
941
+ 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})*$/;
942
+ return regex.test(value);
943
+ };
944
+
945
+ validatorHandlers.ip = function (value: any, element: HTMLElement) {
946
+ 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]?))*$/;
947
+ return regex.test(value);
948
+ };
949
+
950
+ validatorHandlers['password-confirm'] = function (value: any, element: HTMLElement) {
951
+ const selector = element.dataset.confirmTarget;
952
+
953
+ if (!selector) {
954
+ throw new Error('Validator: "password-confirm" must add "data-confirm-target" attribute.');
955
+ }
956
+
957
+ const target = document.querySelector<HTMLInputElement>(selector);
958
+
959
+ return target?.value === value;
960
+ };
961
+
962
+ export { validatorHandlers as validators };
963
+
964
+ // customElements.define(UnicornFormValidateElement.is, UnicornFormValidateElement);
965
+ // customElements.define(UnicornFieldValidateElement.is, UnicornFieldValidateElement);
966
+
967
+ export const ready = Promise.all([
968
+ useUniDirective('form-validate', {
969
+ mounted(el, binding) {
970
+ getBoundedInstance(el, 'form.validation', (ele) => {
971
+ return new UnicornFormValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
972
+ });
973
+ },
974
+ updated(el, binding) {
975
+ const instance = getBoundedInstance<UnicornFormValidation>(el, 'form.validation');
976
+ instance.mergeOptions(JSON.parse(binding.value || '{}'));
977
+ }
978
+ }),
979
+
980
+ useUniDirective('field-validate', {
981
+ mounted(el, binding) {
982
+ getBoundedInstance<UnicornFieldValidation>(el, 'field.validation', (ele) => {
983
+ return new UnicornFieldValidation(ele as HTMLElement, JSON.parse(binding.value || '{}'));
984
+ });
985
+ },
986
+
987
+ updated(el, binding) {
988
+ const instance = getBoundedInstance<UnicornFieldValidation>(el, 'field.validation');
989
+ instance.mergeOptions(JSON.parse(binding.value || '{}') || {});
990
+ }
991
+ })
992
+ ]);
993
+
994
+ function handleParamValue(value: any) {
995
+ if (!isNaN(Number(value))) {
996
+ return Number(value);
997
+ }
998
+
999
+ if (value === 'null') {
1000
+ return null;
1001
+ }
1002
+
1003
+ if (value === 'true') {
1004
+ return true;
1005
+ }
1006
+
1007
+ if (value === 'false') {
1008
+ return true;
1009
+ }
1010
+
1011
+ return value;
1012
+ }
1013
+
1014
+ export interface ValidationModule {
1015
+ UnicornFormValidation: typeof UnicornFormValidation;
1016
+ UnicornFieldValidation: typeof UnicornFieldValidation;
1017
+ ready: Promise<any>;
1018
+ validators: typeof validatorHandlers;
1019
+ }