@windwalker-io/unicorn-next 0.1.0

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 (175) hide show
  1. package/.editorconfig +18 -0
  2. package/.gulp.json +7 -0
  3. package/bin/release.mjs +47 -0
  4. package/dist/chunks/_arrayPush.js +168 -0
  5. package/dist/chunks/_arrayPush.js.map +1 -0
  6. package/dist/chunks/_baseRest.js +73 -0
  7. package/dist/chunks/_baseRest.js.map +1 -0
  8. package/dist/chunks/_commonjsHelpers.js +7 -0
  9. package/dist/chunks/_commonjsHelpers.js.map +1 -0
  10. package/dist/chunks/_getPrototype.js +130 -0
  11. package/dist/chunks/_getPrototype.js.map +1 -0
  12. package/dist/chunks/button-radio.js +147 -0
  13. package/dist/chunks/button-radio.js.map +1 -0
  14. package/dist/chunks/checkboxes-multi-select.js +44 -0
  15. package/dist/chunks/checkboxes-multi-select.js.map +1 -0
  16. package/dist/chunks/cloneDeep.js +287 -0
  17. package/dist/chunks/cloneDeep.js.map +1 -0
  18. package/dist/chunks/cropper.min.js +5 -0
  19. package/dist/chunks/cropper.min.js.map +1 -0
  20. package/dist/chunks/field-cascade-select.js +256 -0
  21. package/dist/chunks/field-cascade-select.js.map +1 -0
  22. package/dist/chunks/field-file-drag.js +218 -0
  23. package/dist/chunks/field-file-drag.js.map +1 -0
  24. package/dist/chunks/field-flatpickr.js +893 -0
  25. package/dist/chunks/field-flatpickr.js.map +1 -0
  26. package/dist/chunks/field-modal-select.js +403 -0
  27. package/dist/chunks/field-modal-select.js.map +1 -0
  28. package/dist/chunks/field-modal-tree.js +790 -0
  29. package/dist/chunks/field-modal-tree.js.map +1 -0
  30. package/dist/chunks/field-multi-uploader.js +256 -0
  31. package/dist/chunks/field-multi-uploader.js.map +1 -0
  32. package/dist/chunks/field-repeatable.js +132 -0
  33. package/dist/chunks/field-repeatable.js.map +1 -0
  34. package/dist/chunks/field-single-image-drag.js +338 -0
  35. package/dist/chunks/field-single-image-drag.js.map +1 -0
  36. package/dist/chunks/form.js +154 -0
  37. package/dist/chunks/form.js.map +1 -0
  38. package/dist/chunks/grid.js +345 -0
  39. package/dist/chunks/grid.js.map +1 -0
  40. package/dist/chunks/http-client.js +229 -0
  41. package/dist/chunks/http-client.js.map +1 -0
  42. package/dist/chunks/iframe-modal.js +124 -0
  43. package/dist/chunks/iframe-modal.js.map +1 -0
  44. package/dist/chunks/index.js +309 -0
  45. package/dist/chunks/index.js.map +1 -0
  46. package/dist/chunks/isArguments.js +146 -0
  47. package/dist/chunks/isArguments.js.map +1 -0
  48. package/dist/chunks/keep-tab.js +101 -0
  49. package/dist/chunks/keep-tab.js.map +1 -0
  50. package/dist/chunks/legacy.js +210 -0
  51. package/dist/chunks/legacy.js.map +1 -0
  52. package/dist/chunks/list-dependent.js +231 -0
  53. package/dist/chunks/list-dependent.js.map +1 -0
  54. package/dist/chunks/s3-multipart-uploader.js +172 -0
  55. package/dist/chunks/s3-multipart-uploader.js.map +1 -0
  56. package/dist/chunks/s3-uploader.js +136 -0
  57. package/dist/chunks/s3-uploader.js.map +1 -0
  58. package/dist/chunks/show-on.js +237 -0
  59. package/dist/chunks/show-on.js.map +1 -0
  60. package/dist/chunks/tinymce.js +196 -0
  61. package/dist/chunks/tinymce.js.map +1 -0
  62. package/dist/chunks/ui-bootstrap5.js +71 -0
  63. package/dist/chunks/ui-bootstrap5.js.map +1 -0
  64. package/dist/chunks/unicorn.js +2202 -0
  65. package/dist/chunks/unicorn.js.map +1 -0
  66. package/dist/chunks/validation.js +854 -0
  67. package/dist/chunks/validation.js.map +1 -0
  68. package/dist/editor.css +1 -0
  69. package/dist/index.d.ts +1427 -0
  70. package/dist/multi-level-menu.css +1 -0
  71. package/dist/switcher.css +1 -0
  72. package/dist/unicorn-next.css +12 -0
  73. package/dist/unicorn.js +125 -0
  74. package/dist/unicorn.js.map +1 -0
  75. package/fusionfile.mjs +155 -0
  76. package/images/ajax-loader.gif +0 -0
  77. package/images/placeholder/avatar.png +0 -0
  78. package/images/placeholder/image-16x10.png +0 -0
  79. package/images/placeholder/image-16x9.png +0 -0
  80. package/images/placeholder/image-1x1.png +0 -0
  81. package/images/placeholder/image-4x3.png +0 -0
  82. package/package.json +102 -0
  83. package/scss/bootstrap/multi-level-menu.scss +121 -0
  84. package/scss/editor.scss +116 -0
  85. package/scss/field/file-drag.scss +102 -0
  86. package/scss/field/single-image-drag.scss +88 -0
  87. package/scss/field/vue-drag-uploader.scss +160 -0
  88. package/scss/switcher.scss +156 -0
  89. package/src/app.ts +128 -0
  90. package/src/bootstrap/button-radio.ts +208 -0
  91. package/src/bootstrap/keep-tab.ts +155 -0
  92. package/src/composable/index.ts +21 -0
  93. package/src/composable/useCheckboxesMultiSelect.ts +22 -0
  94. package/src/composable/useFieldCascadeSelect.ts +9 -0
  95. package/src/composable/useFieldFileDrag.ts +9 -0
  96. package/src/composable/useFieldFlatpickr.ts +3 -0
  97. package/src/composable/useFieldModalSelect.ts +6 -0
  98. package/src/composable/useFieldModalTree.ts +3 -0
  99. package/src/composable/useFieldMultiUploader.ts +3 -0
  100. package/src/composable/useFieldRepeatable.ts +9 -0
  101. package/src/composable/useFieldSingleImageDrag.ts +5 -0
  102. package/src/composable/useForm.ts +43 -0
  103. package/src/composable/useGrid.ts +57 -0
  104. package/src/composable/useHttp.ts +8 -0
  105. package/src/composable/useIframeModal.ts +9 -0
  106. package/src/composable/useListDependent.ts +26 -0
  107. package/src/composable/useQueue.ts +13 -0
  108. package/src/composable/useS3Uploader.ts +32 -0
  109. package/src/composable/useShowOn.ts +9 -0
  110. package/src/composable/useStack.ts +13 -0
  111. package/src/composable/useTinymce.ts +29 -0
  112. package/src/composable/useTomSelect.ts +72 -0
  113. package/src/composable/useUIBootstrap5.ts +48 -0
  114. package/src/composable/useUniDirective.ts +32 -0
  115. package/src/composable/useValidation.ts +39 -0
  116. package/src/data.ts +36 -0
  117. package/src/events.ts +73 -0
  118. package/src/legacy/legacy.ts +186 -0
  119. package/src/legacy/loader.ts +125 -0
  120. package/src/module/checkboxes-multi-select.ts +54 -0
  121. package/src/module/field-cascade-select.ts +292 -0
  122. package/src/module/field-file-drag.ts +292 -0
  123. package/src/module/field-flatpickr.ts +127 -0
  124. package/src/module/field-modal-select.ts +174 -0
  125. package/src/module/field-modal-tree.ts +27 -0
  126. package/src/module/field-multi-uploader.ts +361 -0
  127. package/src/module/field-repeatable.ts +202 -0
  128. package/src/module/field-single-image-drag.ts +468 -0
  129. package/src/module/form.ts +223 -0
  130. package/src/module/grid.ts +465 -0
  131. package/src/module/http-client.ts +243 -0
  132. package/src/module/iframe-modal.ts +167 -0
  133. package/src/module/list-dependent.ts +321 -0
  134. package/src/module/s3-multipart-uploader.ts +300 -0
  135. package/src/module/s3-uploader.ts +234 -0
  136. package/src/module/show-on.ts +173 -0
  137. package/src/module/tinymce.ts +263 -0
  138. package/src/module/ui-bootstrap5.ts +107 -0
  139. package/src/module/validation.ts +1019 -0
  140. package/src/plugin/index.ts +1 -0
  141. package/src/plugin/php-adapter.ts +65 -0
  142. package/src/polyfill/form-request-submit.ts +31 -0
  143. package/src/polyfill/index.ts +9 -0
  144. package/src/service/animate.ts +58 -0
  145. package/src/service/crypto.ts +27 -0
  146. package/src/service/dom-watcher.ts +62 -0
  147. package/src/service/dom.ts +265 -0
  148. package/src/service/helper.ts +48 -0
  149. package/src/service/index.ts +10 -0
  150. package/src/service/lang.ts +122 -0
  151. package/src/service/loader.ts +152 -0
  152. package/src/service/router.ts +118 -0
  153. package/src/service/ui.ts +497 -0
  154. package/src/service/uri.ts +106 -0
  155. package/src/types/base.ts +9 -0
  156. package/src/types/index.ts +4 -0
  157. package/src/types/modal-tree.ts +12 -0
  158. package/src/types/plugin.ts +6 -0
  159. package/src/types/shims.d.ts +18 -0
  160. package/src/types/ui.ts +6 -0
  161. package/src/unicorn.ts +63 -0
  162. package/src/utilities/arr.ts +25 -0
  163. package/src/utilities/base.ts +9 -0
  164. package/src/utilities/data.ts +48 -0
  165. package/src/utilities/index.ts +5 -0
  166. package/src/utilities/tree.ts +20 -0
  167. package/src/vue/components/ModalTree/ModalTreeApp.vue +175 -0
  168. package/src/vue/components/ModalTree/TreeItem.vue +262 -0
  169. package/src/vue/components/ModalTree/TreeModal.vue +225 -0
  170. package/tests/test.js +4 -0
  171. package/tsconfig.js.json +25 -0
  172. package/tsconfig.json +17 -0
  173. package/vite.assets.config.ts +61 -0
  174. package/vite.config.test.ts +36 -0
  175. package/vite.config.ts +112 -0
@@ -0,0 +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
+ }