@kupola/kupola 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/validation.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { getValidationConfig } from './kupola-config.js';
2
+ import { getPerformanceConfig } from './kupola-config.js';
3
+
1
4
  class KupolaValidator {
2
5
  constructor() {
3
6
  this.validators = {
@@ -12,9 +15,9 @@ class KupolaValidator {
12
15
  equalTo: this.validateEqualTo,
13
16
  phone: this.validatePhone,
14
17
  date: this.validateDate,
15
- number: this.validateNumber
18
+ number: this.validateNumber,
16
19
  };
17
-
20
+
18
21
  this.customValidators = {};
19
22
  this.asyncValidators = {};
20
23
  this.customAsyncValidators = {};
@@ -35,13 +38,13 @@ class KupolaValidator {
35
38
  const errors = {};
36
39
  const inputs = form.querySelectorAll('[data-validate]');
37
40
  let hasError = false;
38
-
41
+
39
42
  inputs.forEach(input => {
40
43
  const name = input.name || input.id;
41
44
  const rules = this.parseRules(input.getAttribute('data-validate'));
42
45
  const value = this.getValue(input);
43
-
44
- for (const [rule, params] of Object.entries(rules)) {
46
+
47
+ for (const [ rule, params ] of Object.entries(rules)) {
45
48
  const validatorFunc = this.customValidators[rule] || this.validators[rule];
46
49
  const isValid = validatorFunc?.(value, params);
47
50
  if (!isValid) {
@@ -54,15 +57,15 @@ class KupolaValidator {
54
57
  }
55
58
  }
56
59
  });
57
-
60
+
58
61
  this.formStates[formId] = {
59
62
  valid: !hasError,
60
63
  errors: errors,
61
- errorCount: Object.keys(errors).length
64
+ errorCount: Object.keys(errors).length,
62
65
  };
63
-
66
+
64
67
  this.updateFormState(form);
65
-
68
+
66
69
  return !hasError;
67
70
  }
68
71
 
@@ -70,27 +73,27 @@ class KupolaValidator {
70
73
  if (input.classList.contains('ds-datepicker__input') || input.classList.contains('ds-timepicker__input')) {
71
74
  return input.value.trim();
72
75
  }
73
-
76
+
74
77
  if (input.closest('.ds-select')) {
75
78
  const select = input.closest('.ds-select');
76
79
  const valueEl = select.querySelector('.ds-select__value') || select.querySelector('.ds-select__trigger span');
77
80
  return valueEl ? valueEl.textContent.trim() : '';
78
81
  }
79
-
82
+
80
83
  if (input.closest('.ds-fileupload')) {
81
84
  const upload = input.closest('.ds-fileupload');
82
85
  const uploadInstance = upload.__fileUploadInstance;
83
86
  return uploadInstance && uploadInstance.getFiles().length > 0 ? 'has-files' : '';
84
87
  }
85
-
88
+
86
89
  return input.value.trim();
87
90
  }
88
91
 
89
92
  validateInput(input) {
90
93
  const rules = this.parseRules(input.getAttribute('data-validate'));
91
94
  const value = this.getValue(input);
92
-
93
- for (const [rule, params] of Object.entries(rules)) {
95
+
96
+ for (const [ rule, params ] of Object.entries(rules)) {
94
97
  const validatorFunc = this.customValidators[rule] || this.validators[rule];
95
98
  const isValid = validatorFunc?.(value, params);
96
99
  if (!isValid) {
@@ -98,7 +101,7 @@ class KupolaValidator {
98
101
  return false;
99
102
  }
100
103
  }
101
-
104
+
102
105
  this.clearError(input);
103
106
  return true;
104
107
  }
@@ -106,13 +109,13 @@ class KupolaValidator {
106
109
  validateAll() {
107
110
  const forms = document.querySelectorAll('form[data-validation]');
108
111
  let allValid = true;
109
-
112
+
110
113
  forms.forEach(form => {
111
114
  if (!this.validate(form)) {
112
115
  allValid = false;
113
116
  }
114
117
  });
115
-
118
+
116
119
  return allValid;
117
120
  }
118
121
 
@@ -120,19 +123,19 @@ class KupolaValidator {
120
123
  const formId = form.id || `form-${Math.random().toString(36).substr(2, 9)}`;
121
124
  const errors = {};
122
125
  const group = options.group;
123
- const inputs = group
126
+ const inputs = group
124
127
  ? form.querySelectorAll(`[data-validate][data-validate-group="${group}"]`)
125
128
  : form.querySelectorAll('[data-validate]');
126
-
129
+
127
130
  let hasError = false;
128
-
131
+
129
132
  for (const input of inputs) {
130
133
  const isValid = await this.validateInputAsync(input);
131
134
  if (!isValid) {
132
135
  hasError = true;
133
136
  }
134
137
  }
135
-
138
+
136
139
  const formErrors = {};
137
140
  inputs.forEach(input => {
138
141
  const name = input.name || input.id;
@@ -141,15 +144,15 @@ class KupolaValidator {
141
144
  formErrors[name] = errorElement.textContent;
142
145
  }
143
146
  });
144
-
147
+
145
148
  this.formStates[formId] = {
146
149
  valid: !hasError,
147
150
  errors: formErrors,
148
- errorCount: Object.keys(formErrors).length
151
+ errorCount: Object.keys(formErrors).length,
149
152
  };
150
-
153
+
151
154
  this.updateFormState(form);
152
-
155
+
153
156
  return !hasError;
154
157
  }
155
158
 
@@ -157,8 +160,8 @@ class KupolaValidator {
157
160
  const rules = this.parseRules(input.getAttribute('data-validate'));
158
161
  const asyncRules = this.parseRules(input.getAttribute('data-validate-async') || '');
159
162
  const value = this.getValue(input);
160
-
161
- for (const [rule, params] of Object.entries(rules)) {
163
+
164
+ for (const [ rule, params ] of Object.entries(rules)) {
162
165
  const validatorFunc = this.customValidators[rule] || this.validators[rule];
163
166
  const isValid = validatorFunc?.(value, params);
164
167
  if (!isValid) {
@@ -166,8 +169,8 @@ class KupolaValidator {
166
169
  return false;
167
170
  }
168
171
  }
169
-
170
- for (const [rule, params] of Object.entries(asyncRules)) {
172
+
173
+ for (const [ rule, params ] of Object.entries(asyncRules)) {
171
174
  const validatorFunc = this.customAsyncValidators[rule] || this.asyncValidators[rule];
172
175
  if (validatorFunc) {
173
176
  try {
@@ -182,7 +185,7 @@ class KupolaValidator {
182
185
  }
183
186
  }
184
187
  }
185
-
188
+
186
189
  this.clearError(input);
187
190
  return true;
188
191
  }
@@ -190,14 +193,14 @@ class KupolaValidator {
190
193
  async validateGroup(form, groupName) {
191
194
  const inputs = form.querySelectorAll(`[data-validate][data-validate-group="${groupName}"]`);
192
195
  let hasError = false;
193
-
196
+
194
197
  for (const input of inputs) {
195
198
  const isValid = await this.validateInputAsync(input);
196
199
  if (!isValid) {
197
200
  hasError = true;
198
201
  }
199
202
  }
200
-
203
+
201
204
  return !hasError;
202
205
  }
203
206
 
@@ -216,7 +219,7 @@ class KupolaValidator {
216
219
 
217
220
  updateFormState(form) {
218
221
  const state = this.getFormState(form);
219
-
222
+
220
223
  if (state.valid) {
221
224
  form.classList.remove('ds-form--invalid');
222
225
  form.classList.add('ds-form--valid');
@@ -224,19 +227,19 @@ class KupolaValidator {
224
227
  form.classList.remove('ds-form--valid');
225
228
  form.classList.add('ds-form--invalid');
226
229
  }
227
-
230
+
228
231
  if (state.loading) {
229
232
  form.classList.add('ds-form--loading');
230
233
  } else {
231
234
  form.classList.remove('ds-form--loading');
232
235
  }
233
-
236
+
234
237
  if (state.submitting) {
235
238
  form.classList.add('ds-form--submitting');
236
239
  } else {
237
240
  form.classList.remove('ds-form--submitting');
238
241
  }
239
-
242
+
240
243
  if (state.disabled) {
241
244
  form.classList.add('ds-form--disabled');
242
245
  form.querySelectorAll('input, select, textarea, button').forEach(el => el.disabled = true);
@@ -248,7 +251,7 @@ class KupolaValidator {
248
251
  }
249
252
  });
250
253
  }
251
-
254
+
252
255
  const statusElement = form.querySelector('.ds-form__status');
253
256
  if (statusElement) {
254
257
  if (state.errorCount > 0) {
@@ -305,12 +308,12 @@ class KupolaValidator {
305
308
  parseRules(rulesString) {
306
309
  const rules = {};
307
310
  const parts = rulesString.split('|');
308
-
311
+
309
312
  parts.forEach(part => {
310
- const [rule, params] = part.split(':');
313
+ const [ rule, params ] = part.split(':');
311
314
  rules[rule] = params ? params.split(',') : [];
312
315
  });
313
-
316
+
314
317
  return rules;
315
318
  }
316
319
 
@@ -328,34 +331,34 @@ class KupolaValidator {
328
331
  return urlRegex.test(value);
329
332
  }
330
333
 
331
- validateMinLength(value, [min]) {
334
+ validateMinLength(value, [ min ]) {
332
335
  return value.length >= parseInt(min);
333
336
  }
334
337
 
335
- validateMaxLength(value, [max]) {
338
+ validateMaxLength(value, [ max ]) {
336
339
  return value.length <= parseInt(max);
337
340
  }
338
341
 
339
- validatePattern(value, [pattern]) {
342
+ validatePattern(value, [ pattern ]) {
340
343
  const regex = new RegExp(pattern);
341
344
  return regex.test(value);
342
345
  }
343
346
 
344
- validateMin(value, [min]) {
347
+ validateMin(value, [ min ]) {
345
348
  return parseFloat(value) >= parseFloat(min);
346
349
  }
347
350
 
348
- validateMax(value, [max]) {
351
+ validateMax(value, [ max ]) {
349
352
  return parseFloat(value) <= parseFloat(max);
350
353
  }
351
354
 
352
- validateEqualTo(value, [targetId]) {
355
+ validateEqualTo(value, [ targetId ]) {
353
356
  const target = document.getElementById(targetId);
354
357
  return target && value === target.value;
355
358
  }
356
359
 
357
360
  validatePhone(value) {
358
- const phoneRegex = /^[\d\s\-\+\(\)]{7,20}$/;
361
+ const phoneRegex = /^[\d\s\-+()]{7,20}$/;
359
362
  return phoneRegex.test(value);
360
363
  }
361
364
 
@@ -372,7 +375,7 @@ class KupolaValidator {
372
375
  input.classList.add('ds-input--error');
373
376
  input.classList.remove('ds-input--success');
374
377
  input.setAttribute('aria-invalid', 'true');
375
-
378
+
376
379
  let errorElement = input.parentElement.querySelector('.ds-input__error');
377
380
  if (!errorElement) {
378
381
  errorElement = document.createElement('span');
@@ -382,21 +385,21 @@ class KupolaValidator {
382
385
  input.parentElement.appendChild(errorElement);
383
386
  }
384
387
  errorElement.textContent = message;
385
-
388
+
386
389
  this.removeStatusIcon(input);
387
-
390
+
388
391
  input.dispatchEvent(new CustomEvent('validation-error', { detail: { message } }));
389
392
  }
390
393
 
391
394
  clearError(input) {
392
395
  input.classList.remove('ds-input--error');
393
396
  input.setAttribute('aria-invalid', 'false');
394
-
397
+
395
398
  const errorElement = input.parentElement.querySelector('.ds-input__error');
396
399
  if (errorElement) {
397
400
  errorElement.remove();
398
401
  }
399
-
402
+
400
403
  input.dispatchEvent(new CustomEvent('validation-success'));
401
404
  }
402
405
 
@@ -404,13 +407,13 @@ class KupolaValidator {
404
407
  input.classList.add('ds-input--success');
405
408
  input.classList.remove('ds-input--error');
406
409
  input.setAttribute('aria-invalid', 'false');
407
-
410
+
408
411
  this.removeStatusIcon(input);
409
-
412
+
410
413
  const icon = document.createElement('span');
411
414
  icon.className = 'ds-input__status-icon ds-input__status-icon--success';
412
415
  icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>';
413
-
416
+
414
417
  input.parentElement.appendChild(icon);
415
418
  }
416
419
 
@@ -423,8 +426,8 @@ class KupolaValidator {
423
426
 
424
427
  getErrorMessage(rule, params, input) {
425
428
  const customMessage = input.getAttribute(`data-message-${rule}`);
426
- if (customMessage) return customMessage;
427
-
429
+ if (customMessage) {return customMessage;}
430
+
428
431
  const messages = {
429
432
  required: 'This field is required',
430
433
  email: 'Please enter a valid email address',
@@ -437,7 +440,7 @@ class KupolaValidator {
437
440
  equalTo: 'Values do not match',
438
441
  phone: 'Please enter a valid phone number',
439
442
  date: 'Please enter a valid date (YYYY-MM-DD)',
440
- number: 'Please enter a valid number'
443
+ number: 'Please enter a valid number',
441
444
  };
442
445
  return messages[rule] || 'Invalid input';
443
446
  }
@@ -448,105 +451,117 @@ const validator = new KupolaValidator();
448
451
  if (!window.__kupolaValidationInitialized) {
449
452
  window.__kupolaValidationInitialized = true;
450
453
  document.addEventListener('DOMContentLoaded', () => {
451
- document.querySelectorAll('form[data-validation]').forEach(form => {
452
- form.addEventListener('submit', async (e) => {
453
- const formId = form.id || `form-${Math.random().toString(36).substr(2, 9)}`;
454
-
455
- if (validator.submitting.has(formId)) {
456
- e.preventDefault();
457
- return;
458
- }
459
-
460
- e.preventDefault();
461
-
462
- const hasAsyncValidation = form.querySelector('[data-validate-async]') !== null;
463
-
464
- let isValid;
465
- if (hasAsyncValidation) {
466
- isValid = await validator.validateAsync(form);
467
- } else {
468
- isValid = validator.validate(form);
469
- }
470
-
471
- if (!isValid) {
472
- const firstError = form.querySelector('.ds-input--error');
473
- if (firstError) {
474
- firstError.focus();
454
+ document.querySelectorAll('form[data-validation]').forEach(form => {
455
+ form.addEventListener('submit', async (e) => {
456
+ const formId = form.id || `form-${Math.random().toString(36).substr(2, 9)}`;
457
+
458
+ if (validator.submitting.has(formId)) {
459
+ e.preventDefault();
460
+ return;
475
461
  }
476
- } else {
477
- validator.submitting.add(formId);
478
- const submitBtn = form.querySelector('button[type="submit"]');
479
- if (submitBtn) {
480
- const originalText = submitBtn.textContent;
481
- submitBtn.setAttribute('data-original-text', originalText);
482
- submitBtn.textContent = 'Submitting...';
483
- submitBtn.disabled = true;
462
+
463
+ e.preventDefault();
464
+
465
+ const hasAsyncValidation = form.querySelector('[data-validate-async]') !== null;
466
+
467
+ let isValid;
468
+ if (hasAsyncValidation) {
469
+ isValid = await validator.validateAsync(form);
470
+ } else {
471
+ isValid = validator.validate(form);
484
472
  }
485
-
486
- try {
487
- const onSubmit = form.getAttribute('data-on-submit');
488
- if (onSubmit && window[onSubmit]) {
489
- await window[onSubmit](form);
490
- } else {
491
- form.submit();
473
+
474
+ if (!isValid) {
475
+ const firstError = form.querySelector('.ds-input--error');
476
+ if (firstError) {
477
+ firstError.focus();
492
478
  }
493
- } finally {
494
- validator.submitting.delete(formId);
479
+ } else {
480
+ validator.submitting.add(formId);
481
+ const submitBtn = form.querySelector('button[type="submit"]');
495
482
  if (submitBtn) {
496
- submitBtn.textContent = submitBtn.getAttribute('data-original-text') || 'Submit';
497
- submitBtn.disabled = false;
483
+ const originalText = submitBtn.textContent;
484
+ submitBtn.setAttribute('data-original-text', originalText);
485
+ submitBtn.textContent = 'Submitting...';
486
+ submitBtn.disabled = true;
498
487
  }
499
- }
500
- }
501
- });
502
488
 
503
- form.querySelectorAll('[data-validate]').forEach(input => {
504
- input.addEventListener('blur', () => {
505
- setTimeout(() => {
506
- const activeElement = document.activeElement;
507
- if (activeElement && activeElement.closest('.ds-select')) {
508
- return;
489
+ try {
490
+ const onSubmit = form.getAttribute('data-on-submit');
491
+ if (onSubmit && window[onSubmit]) {
492
+ await window[onSubmit](form);
493
+ } else {
494
+ form.submit();
495
+ }
496
+ } finally {
497
+ validator.submitting.delete(formId);
498
+ if (submitBtn) {
499
+ submitBtn.textContent = submitBtn.getAttribute('data-original-text') || 'Submit';
500
+ submitBtn.disabled = false;
501
+ }
509
502
  }
510
- const isValid = validator.validateInput(input);
511
- if (isValid && input.value.trim()) {
512
- validator.showSuccess(input);
513
- }
514
- }, 50);
503
+ }
515
504
  });
516
505
 
517
- const debounce = (func, wait) => {
518
- let timeout;
519
- return (...args) => {
520
- clearTimeout(timeout);
521
- timeout = setTimeout(() => func(...args), wait);
506
+ form.querySelectorAll('[data-validate]').forEach(input => {
507
+ const validationConfig = getValidationConfig();
508
+ const trigger = validationConfig.trigger || 'blur';
509
+
510
+ const validateAndShow = () => {
511
+ if (!validationConfig.showErrors) {return;}
512
+ setTimeout(() => {
513
+ const activeElement = document.activeElement;
514
+ if (activeElement && activeElement.closest('.ds-select')) {
515
+ return;
516
+ }
517
+ const isValid = validator.validateInput(input);
518
+ if (isValid && input.value.trim()) {
519
+ validator.showSuccess(input);
520
+ }
521
+ }, 50);
522
522
  };
523
- };
524
-
525
- const validateOnInput = debounce(() => {
526
- const value = validator.getValue(input);
527
- if (value.length > 0 || input.classList.contains('ds-input--error')) {
528
- const isValid = validator.validateInput(input);
529
- if (isValid && value) {
530
- validator.showSuccess(input);
531
- }
532
- } else {
533
- validator.removeStatusIcon(input);
523
+
524
+ if (trigger === 'blur' || trigger === 'both') {
525
+ input.addEventListener('blur', validateAndShow);
534
526
  }
535
- }, 300);
536
527
 
537
- input.addEventListener('input', validateOnInput);
528
+ const debounce = (func, wait) => {
529
+ let timeout;
530
+ return (...args) => {
531
+ clearTimeout(timeout);
532
+ timeout = setTimeout(() => func(...args), wait);
533
+ };
534
+ };
538
535
 
539
- input.addEventListener('keyup', (e) => {
540
- if (e.key === 'Enter') {
541
- const isValid = validator.validateInput(input);
542
- if (isValid && input.value.trim()) {
543
- validator.showSuccess(input);
536
+ const performanceConfig = getPerformanceConfig();
537
+ const validateOnInput = debounce(() => {
538
+ if (!validationConfig.showErrors) {return;}
539
+ const value = validator.getValue(input);
540
+ if (value.length > 0 || input.classList.contains('ds-input--error')) {
541
+ const isValid = validator.validateInput(input);
542
+ if (isValid && value) {
543
+ validator.showSuccess(input);
544
+ }
545
+ } else {
546
+ validator.removeStatusIcon(input);
544
547
  }
548
+ }, performanceConfig.debounceDelay);
549
+
550
+ if (trigger === 'input' || trigger === 'both') {
551
+ input.addEventListener('input', validateOnInput);
545
552
  }
553
+
554
+ input.addEventListener('keyup', (e) => {
555
+ if (e.key === 'Enter') {
556
+ const isValid = validator.validateInput(input);
557
+ if (isValid && input.value.trim()) {
558
+ validator.showSuccess(input);
559
+ }
560
+ }
561
+ });
546
562
  });
547
563
  });
548
564
  });
549
- });
550
565
  }
551
566
 
552
- export { KupolaValidator, validator };
567
+ export { KupolaValidator, validator };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kupola/kupola",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "A lightweight UI toolkit for any web project. No heavy frontend frameworks required.",
5
5
  "main": "dist/kupola.cjs.js",
6
6
  "module": "dist/kupola.esm.js",
@@ -120,6 +120,7 @@
120
120
  "@rollup/plugin-terser": "^0.4.4",
121
121
  "eslint": "^8.57.0",
122
122
  "jest": "^29.7.0",
123
+ "jest-environment-jsdom": "^30.4.1",
123
124
  "prettier": "^3.2.5",
124
125
  "rollup": "^4.13.0",
125
126
  "rollup-plugin-copy": "^3.5.0",