client_side_validations 24.0.0 → 25.0.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.
@@ -1,789 +1,625 @@
1
1
  /*!
2
- * Client Side Validations JS - v24.0.0 (https://github.com/DavyJonesLocker/client_side_validations)
2
+ * Client Side Validations JS - v25.0.0 (https://github.com/DavyJonesLocker/client_side_validations)
3
3
  * Copyright (c) 2026 Geremia Taglialatela, Brian Cardarella
4
4
  * Licensed under MIT (https://opensource.org/licenses/mit-license.php)
5
5
  */
6
-
7
- (function (global, factory) {
8
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
9
- typeof define === 'function' && define.amd ? define(factory) :
10
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ClientSideValidations = factory());
11
- })(this, (function () { 'use strict';
12
-
13
- const boundEventListeners = new WeakMap();
14
- const addBoundEventListener = (element, eventName, listener) => {
15
- element.addEventListener(eventName, listener);
16
- const listeners = boundEventListeners.get(element) || [];
17
- listeners.push({
18
- eventName,
19
- listener
20
- });
21
- boundEventListeners.set(element, listeners);
22
- };
23
- const bindElementEvents = (element, eventsToBind) => {
24
- for (const eventName in eventsToBind) {
25
- addBoundEventListener(element, eventName, eventsToBind[eventName]);
26
- }
27
- };
28
- const clearBoundEventListeners = element => {
29
- const listeners = boundEventListeners.get(element);
30
- if (!listeners) {
31
- return;
32
- }
33
- listeners.forEach(_ref => {
34
- let {
35
- eventName,
36
- listener
37
- } = _ref;
38
- element.removeEventListener(eventName, listener);
39
- });
40
- boundEventListeners.delete(element);
41
- };
42
- const dispatchCustomEvent = (element, eventName, detail) => {
43
- element.dispatchEvent(new CustomEvent(eventName, {
44
- bubbles: true,
45
- detail
46
- }));
47
- };
48
-
49
- const arrayHasValue = (value, otherValues) => {
50
- for (let i = 0, l = otherValues.length; i < l; i++) {
51
- if (value === otherValues[i]) {
52
- return true;
53
- }
54
- }
55
- return false;
56
- };
57
- const createElementFromHTML = html => {
58
- const element = document.createElement('div');
59
- element.innerHTML = html;
60
- return element.firstChild;
61
- };
62
- const isDOMCollection = target => {
63
- return Array.isArray(target) || typeof NodeList !== 'undefined' && target instanceof NodeList || typeof HTMLCollection !== 'undefined' && target instanceof HTMLCollection || typeof RadioNodeList !== 'undefined' && target instanceof RadioNodeList;
64
- };
65
- const isDOMElement = target => {
66
- return target != null && target.nodeType === 1;
67
- };
68
- const getDOMElements = target => {
69
- if (target == null) {
70
- return [];
71
- }
72
- if (isDOMElement(target)) {
73
- return [target];
74
- }
75
- if (isDOMCollection(target)) {
76
- return Array.from(target).filter(isDOMElement);
77
- }
78
- return [];
79
- };
80
- const isFormElement = element => {
81
- return element.tagName === 'FORM';
82
- };
83
- const isInputElement = element => {
84
- switch (element.tagName) {
85
- case 'INPUT':
86
- return element.type !== 'submit' && element.type !== 'button';
87
- case 'SELECT':
88
- case 'TEXTAREA':
89
- return true;
90
- default:
91
- return false;
92
- }
93
- };
94
- const isVisible = element => {
95
- return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
96
- };
97
- const isValuePresent = value => {
98
- return !/^\s*$/.test(value || '');
99
- };
100
-
101
- const isNamedInputElement = element => {
102
- return isInputElement(element) && element.name != null && element.name !== '';
103
- };
104
- const getFormControls = form => {
105
- return Array.from(form.elements).filter(isInputElement);
106
- };
107
- const getFormInputs = form => {
108
- return getFormControls(form).filter(element => {
109
- return isNamedInputElement(element) && !element.disabled && isVisible(element);
110
- });
111
- };
112
- const findFormElementByName = (form, name) => {
113
- return getFormControls(form).find(element => element.name === name);
114
- };
115
- const enableForm = form => {
116
- ClientSideValidations.enablers.form(form);
117
- };
118
- const enableForms = () => {
119
- document.querySelectorAll(ClientSideValidations.selectors.forms).forEach(enableForm);
120
- };
121
- const ClientSideValidations = {
122
- callbacks: {
123
- element: {
124
- after: (element, eventData) => {},
125
- before: (element, eventData) => {},
126
- fail: (element, message, addError, eventData) => addError(),
127
- pass: (element, removeError, eventData) => removeError()
128
- },
129
- form: {
130
- after: (form, eventData) => {},
131
- before: (form, eventData) => {},
132
- fail: (form, eventData) => {},
133
- pass: (form, eventData) => {}
134
- }
135
- },
136
- eventsToBind: {
137
- form: form => ({
138
- submit: eventData => {
139
- if (!ClientSideValidations.isValid(form, form.ClientSideValidations.settings.validators)) {
140
- eventData.preventDefault();
141
- eventData.stopImmediatePropagation();
142
- }
143
- },
144
- 'ajax:beforeSend': function (eventData) {
145
- if (eventData.target === this) {
146
- ClientSideValidations.isValid(form, form.ClientSideValidations.settings.validators);
147
- }
148
- },
149
- 'form:validate:after': eventData => {
150
- ClientSideValidations.callbacks.form.after(form, eventData);
151
- },
152
- 'form:validate:before': eventData => {
153
- ClientSideValidations.callbacks.form.before(form, eventData);
154
- },
155
- 'form:validate:fail': eventData => {
156
- ClientSideValidations.callbacks.form.fail(form, eventData);
157
- },
158
- 'form:validate:pass': eventData => {
159
- ClientSideValidations.callbacks.form.pass(form, eventData);
160
- }
161
- }),
162
- input: form => ({
163
- focusout: function () {
164
- ClientSideValidations.isValid(this, form.ClientSideValidations.settings.validators);
165
- },
166
- change: function () {
167
- this.dataset.csvChanged = 'true';
168
- },
169
- 'element:validate:after': function (eventData) {
170
- ClientSideValidations.callbacks.element.after(this, eventData);
171
- },
172
- 'element:validate:before': function (eventData) {
173
- ClientSideValidations.callbacks.element.before(this, eventData);
174
- },
175
- 'element:validate:fail': function (eventData) {
176
- const element = this;
177
- const message = eventData.detail;
178
- ClientSideValidations.callbacks.element.fail(element, message, function () {
179
- form.ClientSideValidations.addError(element, message);
180
- }, eventData);
181
- },
182
- 'element:validate:pass': function (eventData) {
183
- const element = this;
184
- ClientSideValidations.callbacks.element.pass(element, function () {
185
- form.ClientSideValidations.removeError(element);
186
- }, eventData);
187
- }
188
- }),
189
- inputConfirmation: (elementToConfirm, form) => ({
190
- focusout: () => {
191
- elementToConfirm.dataset.csvChanged = 'true';
192
- ClientSideValidations.isValid(elementToConfirm, form.ClientSideValidations.settings.validators);
193
- },
194
- keyup: () => {
195
- elementToConfirm.dataset.csvChanged = 'true';
196
- ClientSideValidations.isValid(elementToConfirm, form.ClientSideValidations.settings.validators);
197
- }
198
- })
199
- },
200
- enablers: {
201
- form: form => {
202
- clearBoundEventListeners(form);
203
- getFormControls(form).forEach(clearBoundEventListeners);
204
- form.ClientSideValidations = {
205
- settings: JSON.parse(form.dataset.clientSideValidations),
206
- addError: (element, message) => ClientSideValidations.formBuilders[form.ClientSideValidations.settings.html_settings.type].add(element, form.ClientSideValidations.settings.html_settings, message),
207
- removeError: element => ClientSideValidations.formBuilders[form.ClientSideValidations.settings.html_settings.type].remove(element, form.ClientSideValidations.settings.html_settings)
208
- };
209
- bindElementEvents(form, ClientSideValidations.eventsToBind.form(form));
210
- getFormInputs(form).forEach(element => {
211
- ClientSideValidations.enablers.input(element);
212
- });
213
- },
214
- input: function (input) {
215
- const form = input.form;
216
- if (!form) {
217
- return;
218
- }
219
- clearBoundEventListeners(input);
220
- const eventsToBind = ClientSideValidations.eventsToBind.input(form);
221
- if (input.type !== 'radio' && !(input.id && input.id.endsWith('_confirmation'))) {
222
- input.dataset.csvValidate = 'true';
223
- bindElementEvents(input, eventsToBind);
224
- }
225
- if (input.type === 'checkbox') {
226
- bindElementEvents(input, {
227
- change: function () {
228
- ClientSideValidations.isValid(this, form.ClientSideValidations.settings.validators);
229
- }
230
- });
231
- }
232
- if (input.id && input.id.endsWith('_confirmation')) {
233
- const elementToConfirm = document.getElementById(input.id.match(/(.+)_confirmation/)[1]);
234
- if (elementToConfirm && elementToConfirm.form === form) {
235
- bindElementEvents(input, ClientSideValidations.eventsToBind.inputConfirmation(elementToConfirm, form));
236
- }
237
- }
238
- }
239
- },
240
- formBuilders: {
241
- 'ActionView::Helpers::FormBuilder': {
242
- add: (element, settings, message) => {
243
- if (!element) {
244
- return;
245
- }
246
- const form = element.form;
247
- const inputErrorTemplate = createElementFromHTML(settings.input_tag);
248
- let inputErrorElement = element.closest(".".concat(inputErrorTemplate.getAttribute('class').replace(/ /g, '.')));
249
- if (!inputErrorElement) {
250
- inputErrorElement = inputErrorTemplate;
251
- if (element.getAttribute('autofocus')) {
252
- element.setAttribute('autofocus', false);
253
- }
254
- element.before(inputErrorElement);
255
- inputErrorElement.querySelector('span#input_tag').replaceWith(element);
256
- const inputErrorLabelMessageElement = inputErrorElement.querySelector('label.message');
257
- if (inputErrorLabelMessageElement) {
258
- inputErrorLabelMessageElement.setAttribute('for', element.id);
259
- }
260
- }
261
- const labelElement = form.querySelector("label[for=\"".concat(element.id, "\"]:not(.message)"));
262
- if (labelElement) {
263
- const labelErrorTemplate = createElementFromHTML(settings.label_tag);
264
- const labelErrorContainer = labelElement.closest(".".concat(labelErrorTemplate.getAttribute('class').replace(/ /g, '.')));
265
- if (!labelErrorContainer) {
266
- labelElement.after(labelErrorTemplate);
267
- labelErrorTemplate.querySelector('label#label_tag').replaceWith(labelElement);
268
- }
269
- }
270
- const labelMessageElement = form.querySelector("label.message[for=\"".concat(element.id, "\"]"));
271
- if (labelMessageElement) {
272
- labelMessageElement.textContent = message;
273
- }
274
- },
275
- remove: (element, settings) => {
276
- if (!element) {
277
- return;
278
- }
279
- const form = element.form;
280
- const inputErrorClass = createElementFromHTML(settings.input_tag).getAttribute('class');
281
- const inputErrorElement = element.closest(".".concat(inputErrorClass.replace(/ /g, '.')));
282
- if (inputErrorElement) {
283
- inputErrorElement.querySelector("#".concat(element.id)).remove();
284
- inputErrorElement.replaceWith(element);
285
- }
286
- const labelElement = form.querySelector("label[for=\"".concat(element.id, "\"]:not(.message)"));
287
- if (labelElement) {
288
- const labelErrorClass = createElementFromHTML(settings.label_tag).getAttribute('class');
289
- const labelErrorElement = labelElement.closest(".".concat(labelErrorClass.replace(/ /g, '.')));
290
- if (labelErrorElement) {
291
- labelErrorElement.replaceWith(labelElement);
292
- }
293
- }
294
- const labelMessageElement = form.querySelector("label.message[for=\"".concat(element.id, "\"]"));
295
- if (labelMessageElement) {
296
- labelMessageElement.remove();
297
- }
298
- }
299
- }
300
- },
301
- patterns: {
302
- numericality: {
303
- default: /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/,
304
- only_integer: /^[+-]?\d+$/
305
- }
306
- },
307
- selectors: {
308
- inputs: 'input:not([type="submit"]):not([type="button"])[name], select[name], textarea[name]',
309
- validate_inputs: 'input[data-csv-validate]:not([type="submit"]):not([type="button"]), select[data-csv-validate], textarea[data-csv-validate]',
310
- forms: 'form[data-client-side-validations]'
311
- },
312
- validators: {
313
- all: () => {
314
- return {
315
- ...ClientSideValidations.validators.local,
316
- ...ClientSideValidations.validators.remote
317
- };
318
- },
319
- local: {},
320
- remote: {}
321
- },
322
- disable: target => {
323
- getDOMElements(target).forEach(element => {
324
- clearBoundEventListeners(element);
325
- if (isFormElement(element)) {
326
- getFormControls(element).forEach(input => {
327
- clearBoundEventListeners(input);
328
- delete input.dataset.csvValid;
329
- delete input.dataset.csvChanged;
330
- delete input.dataset.csvValidate;
331
- });
332
- return;
333
- }
334
- delete element.dataset.csvValid;
335
- delete element.dataset.csvChanged;
336
- if (isInputElement(element)) {
337
- delete element.dataset.csvValidate;
338
- }
339
- });
340
- },
341
- reset: form => {
342
- ClientSideValidations.disable(form);
343
- for (const key in form.ClientSideValidations.settings.validators) {
344
- const element = findFormElementByName(form, key);
345
- if (element) {
346
- form.ClientSideValidations.removeError(element);
347
- }
348
- }
349
- ClientSideValidations.enablers.form(form);
350
- },
351
- initializeOnEvent: () => {
352
- if (window.Turbo != null) {
353
- return 'turbo:load';
354
- } else if (window.Turbolinks != null && window.Turbolinks.supported) {
355
- return window.Turbolinks.EVENTS != null ? 'page:change' : 'turbolinks:load';
356
- }
357
- },
358
- start: () => {
359
- const initializeOnEvent = ClientSideValidations.initializeOnEvent();
360
- if (initializeOnEvent != null) {
361
- document.addEventListener(initializeOnEvent, enableForms);
362
- } else if (document.readyState === 'loading') {
363
- document.addEventListener('DOMContentLoaded', enableForms, {
364
- once: true
365
- });
366
- } else {
367
- enableForms();
368
- }
369
- }
370
- };
371
-
372
- const absenceLocalValidator = (element, options) => {
373
- if (isValuePresent(element.value)) {
374
- return options.message;
375
- }
376
- };
377
- const presenceLocalValidator = (element, options) => {
378
- if (!isValuePresent(element.value)) {
379
- return options.message;
380
- }
381
- };
382
-
383
- const DEFAULT_ACCEPT_OPTION = ['1', true];
384
- const isTextAccepted = (value, acceptOption) => {
385
- if (!acceptOption) {
386
- acceptOption = DEFAULT_ACCEPT_OPTION;
387
- }
388
- if (Array.isArray(acceptOption)) {
389
- return arrayHasValue(value, acceptOption);
390
- }
391
- return value === acceptOption;
392
- };
393
- const acceptanceLocalValidator = (element, options) => {
394
- let valid = true;
395
- if (element.type === 'checkbox') {
396
- valid = element.checked;
397
- }
398
- if (element.type === 'text') {
399
- valid = isTextAccepted(element.value, options.accept);
400
- }
401
- if (!valid) {
402
- return options.message;
403
- }
404
- };
405
-
406
- const isMatching = (value, regExpOptions) => {
407
- return new RegExp(regExpOptions.source, regExpOptions.options).test(value);
408
- };
409
- const hasValidFormat = (value, withOptions, withoutOptions) => {
410
- return withOptions && isMatching(value, withOptions) || withoutOptions && !isMatching(value, withoutOptions);
411
- };
412
- const formatLocalValidator = (element, options) => {
413
- const value = element.value;
414
- if (options.allow_blank && !isValuePresent(value)) {
415
- return;
416
- }
417
- if (!hasValidFormat(value, options.with, options.without)) {
418
- return options.message;
419
- }
420
- };
421
-
422
- const VALIDATIONS$1 = {
423
- even: a => {
424
- return parseInt(a, 10) % 2 === 0;
425
- },
426
- greater_than: (a, b) => {
427
- return parseFloat(a) > parseFloat(b);
428
- },
429
- greater_than_or_equal_to: (a, b) => {
430
- return parseFloat(a) >= parseFloat(b);
431
- },
432
- equal_to: (a, b) => {
433
- return parseFloat(a) === parseFloat(b);
434
- },
435
- less_than: (a, b) => {
436
- return parseFloat(a) < parseFloat(b);
437
- },
438
- less_than_or_equal_to: (a, b) => {
439
- return parseFloat(a) <= parseFloat(b);
440
- },
441
- odd: a => {
442
- return parseInt(a, 10) % 2 === 1;
443
- },
444
- other_than: (a, b) => {
445
- return parseFloat(a) !== parseFloat(b);
446
- }
447
- };
448
- const formatValue = element => {
449
- const value = element.value || '';
450
- const numberFormat = element.form.ClientSideValidations.settings.number_format;
451
- return value.trim().replace(new RegExp("\\".concat(numberFormat.separator), 'g'), '.');
452
- };
453
- const getOtherValue = (validationOption, form) => {
454
- if (!isNaN(parseFloat(validationOption))) {
455
- return validationOption;
456
- }
457
- const validationElements = form.querySelectorAll("[name*=\"".concat(validationOption, "\"]"));
458
- if (validationElements.length === 1) {
459
- const validationElement = validationElements[0];
460
- const otherFormattedValue = formatValue(validationElement);
461
- if (!isNaN(parseFloat(otherFormattedValue))) {
462
- return otherFormattedValue;
463
- }
464
- }
465
- };
466
- const isValid = (validationFunction, validationOption, formattedValue, form) => {
467
- if (validationFunction.length === 2) {
468
- const otherValue = getOtherValue(validationOption, form);
469
- return otherValue == null || otherValue === '' || validationFunction(formattedValue, otherValue);
470
- } else {
471
- return validationFunction(formattedValue);
472
- }
473
- };
474
- const runFunctionValidations = (formattedValue, form, options) => {
475
- for (const validation in VALIDATIONS$1) {
476
- const validationOption = options[validation];
477
- const validationFunction = VALIDATIONS$1[validation];
478
-
479
- // Must check for null because this could be 0
480
- if (validationOption == null) {
481
- continue;
482
- }
483
- if (!isValid(validationFunction, validationOption, formattedValue, form)) {
484
- return options.messages[validation];
485
- }
486
- }
487
- };
488
- const runValidations$1 = (formattedValue, form, options) => {
489
- if (options.only_integer && !ClientSideValidations.patterns.numericality.only_integer.test(formattedValue)) {
490
- return options.messages.only_integer;
491
- }
492
- if (!ClientSideValidations.patterns.numericality.default.test(formattedValue)) {
493
- return options.messages.numericality;
494
- }
495
- return runFunctionValidations(formattedValue, form, options);
496
- };
497
- const numericalityLocalValidator = (element, options) => {
498
- const value = element.value;
499
- if (options.allow_blank && !isValuePresent(value)) {
500
- return;
501
- }
502
- const form = element.form;
503
- const formattedValue = formatValue(element);
504
- return runValidations$1(formattedValue, form, options);
505
- };
506
-
507
- const VALIDATIONS = {
508
- is: (a, b) => {
509
- return a === parseInt(b, 10);
510
- },
511
- minimum: (a, b) => {
512
- return a >= parseInt(b, 10);
513
- },
514
- maximum: (a, b) => {
515
- return a <= parseInt(b, 10);
516
- }
517
- };
518
- const runValidations = (valueLength, options) => {
519
- for (const validation in VALIDATIONS) {
520
- const validationOption = options[validation];
521
- const validationFunction = VALIDATIONS[validation];
522
- if (validationOption && !validationFunction(valueLength, validationOption)) {
523
- return options.messages[validation];
524
- }
525
- }
526
- };
527
- const lengthLocalValidator = (element, options) => {
528
- const value = element.value;
529
- if (options.allow_blank && !isValuePresent(value)) {
530
- return;
531
- }
532
- return runValidations(value.length, options);
533
- };
534
-
535
- const isInList = (value, otherValues) => {
536
- const normalizedOtherValues = [];
537
- for (const otherValueIndex in otherValues) {
538
- normalizedOtherValues.push(otherValues[otherValueIndex].toString());
539
- }
540
- return arrayHasValue(value, normalizedOtherValues);
541
- };
542
- const isInRange = (value, range) => {
543
- return value >= range[0] && value <= range[1];
544
- };
545
- const isIncluded = (value, options, allowBlank) => {
546
- if ((options.allow_blank && !isValuePresent(value)) === allowBlank) {
547
- return true;
548
- }
549
- return options.in && isInList(value, options.in) || options.range && isInRange(value, options.range);
550
- };
551
- const exclusionLocalValidator = (element, options) => {
552
- const value = element.value;
553
- if (isIncluded(value, options, false) || !options.allow_blank && !isValuePresent(value)) {
554
- return options.message;
555
- }
556
- };
557
- const inclusionLocalValidator = (element, options) => {
558
- const value = element.value;
559
- if (!isIncluded(value, options, true)) {
560
- return options.message;
561
- }
562
- };
563
-
564
- const confirmationLocalValidator = (element, options) => {
565
- let value = element.value;
566
- let confirmationValue = document.getElementById("".concat(element.id, "_confirmation")).value;
567
- if (!options.case_sensitive) {
568
- value = value.toLowerCase();
569
- confirmationValue = confirmationValue.toLowerCase();
570
- }
571
- if (value !== confirmationValue) {
572
- return options.message;
573
- }
574
- };
575
-
576
- const isLocallyUnique = (element, value, otherValue, caseSensitive) => {
577
- if (!caseSensitive) {
578
- value = value.toLowerCase();
579
- otherValue = otherValue.toLowerCase();
580
- }
581
- if (otherValue === value) {
582
- element.dataset.csvNotLocallyUnique = 'true';
583
- return false;
584
- }
585
- if (element.dataset.csvNotLocallyUnique) {
586
- delete element.dataset.csvNotLocallyUnique;
587
- element.dataset.csvChanged = 'true';
588
- }
589
- return true;
590
- };
591
- const uniquenessLocalValidator = (element, options) => {
592
- const elementName = element.name;
593
- const matches = elementName.match(/^(.+_attributes\])\[\d+\](.+)$/);
594
- if (!matches) {
595
- return;
596
- }
597
- const form = element.form;
598
- const value = element.value;
599
- let valid = true;
600
- const query = "[name^=\"".concat(matches[1], "\"][name$=\"").concat(matches[2], "\"]:not([name=\"").concat(elementName, "\"])");
601
- const otherElements = form.querySelectorAll(query);
602
- Array.prototype.slice.call(otherElements).forEach(function (otherElement) {
603
- const otherValue = otherElement.value;
604
- if (!isLocallyUnique(otherElement, value, otherValue, options.case_sensitive)) {
605
- valid = false;
606
- }
607
- });
608
- if (!valid) {
609
- return options.message;
610
- }
611
- };
612
-
613
- // Validators will run in the following order
614
- ClientSideValidations.validators.local = {
615
- absence: absenceLocalValidator,
616
- presence: presenceLocalValidator,
617
- acceptance: acceptanceLocalValidator,
618
- format: formatLocalValidator,
619
- numericality: numericalityLocalValidator,
620
- length: lengthLocalValidator,
621
- inclusion: inclusionLocalValidator,
622
- exclusion: exclusionLocalValidator,
623
- confirmation: confirmationLocalValidator,
624
- uniqueness: uniquenessLocalValidator
625
- };
626
- ClientSideValidations.enable = target => {
627
- getDOMElements(target).forEach(element => {
628
- if (isFormElement(element)) {
629
- ClientSideValidations.enablers.form(element);
630
- } else if (isInputElement(element)) {
631
- ClientSideValidations.enablers.input(element);
632
- }
633
- });
634
- return target;
635
- };
636
- ClientSideValidations.validate = target => {
637
- getDOMElements(target).forEach(element => {
638
- if (isFormElement(element)) {
639
- ClientSideValidations.enable(element);
640
- }
641
- });
642
- return target;
643
- };
644
- ClientSideValidations.isValid = (target, validators) => {
645
- const element = getDOMElements(target)[0];
646
- if (!element) {
647
- return true;
648
- }
649
- if (!validators) {
650
- var _form$ClientSideValid;
651
- const form = isFormElement(element) ? element : element.form;
652
- validators = form === null || form === void 0 || (_form$ClientSideValid = form.ClientSideValidations) === null || _form$ClientSideValid === void 0 || (_form$ClientSideValid = _form$ClientSideValid.settings) === null || _form$ClientSideValid === void 0 ? void 0 : _form$ClientSideValid.validators;
653
- }
654
- if (isFormElement(element)) {
655
- return validateForm(element, validators || {});
656
- }
657
- return validateElement(element, validatorsFor(element.name, validators || {}));
658
- };
659
- const cleanNestedElementName = (elementName, nestedMatches, validators) => {
660
- for (const validatorName in validators) {
661
- if (validatorName.match("\\[".concat(nestedMatches[1], "\\].*\\[\\]\\[").concat(nestedMatches[2], "\\]$"))) {
662
- elementName = elementName.replace(/\[[\da-z_]+\]\[(\w+)\]$/g, '[][$1]');
663
- }
664
- }
665
- return elementName;
666
- };
667
- const cleanElementName = (elementName, validators) => {
668
- elementName = elementName.replace(/\[(\w+_attributes)\]\[[\da-z_]+\](?=\[(?:\w+_attributes)\])/g, '[$1][]');
669
- const nestedMatches = elementName.match(/\[(\w+_attributes)\].*\[(\w+)\]$/);
670
- if (nestedMatches) {
671
- elementName = cleanNestedElementName(elementName, nestedMatches, validators);
672
- }
673
- return elementName;
674
- };
675
- const validatorsFor = (elementName, validators) => {
676
- if (!elementName || !validators) {
677
- return {};
678
- }
679
- if (Object.prototype.hasOwnProperty.call(validators, elementName)) {
680
- return validators[elementName];
681
- }
682
- return validators[cleanElementName(elementName, validators)] || {};
683
- };
684
- const getValidationInputs = form => {
685
- return Array.from(form.elements).filter(element => {
686
- if (element.dataset.csvValidate == null || element.disabled) {
687
- return false;
688
- }
689
- return isVisible(element);
690
- });
691
- };
692
- const validateForm = (form, validators) => {
693
- let valid = true;
694
- dispatchCustomEvent(form, 'form:validate:before');
695
- getValidationInputs(form).forEach(element => {
696
- if (!validateElement(element, validatorsFor(element.name, validators))) {
697
- valid = false;
698
- }
699
- });
700
- if (valid) {
701
- dispatchCustomEvent(form, 'form:validate:pass');
702
- } else {
703
- dispatchCustomEvent(form, 'form:validate:fail');
704
- }
705
- dispatchCustomEvent(form, 'form:validate:after');
706
- return valid;
707
- };
708
- const passElement = element => {
709
- dispatchCustomEvent(element, 'element:validate:pass');
710
- delete element.dataset.csvValid;
711
- };
712
- const failElement = (element, message) => {
713
- dispatchCustomEvent(element, 'element:validate:fail', message);
714
- element.dataset.csvValid = 'false';
715
- };
716
- const afterValidate = element => {
717
- dispatchCustomEvent(element, 'element:validate:after');
718
- return element.dataset.csvValid !== 'false';
719
- };
720
- const executeValidator = (validatorFunctions, validatorFunction, validatorOptions, element) => {
721
- for (const validatorOption in validatorOptions) {
722
- if (!validatorOptions[validatorOption]) {
723
- continue;
724
- }
725
- const message = validatorFunction.call(validatorFunctions, element, validatorOptions[validatorOption]);
726
- if (message) {
727
- failElement(element, message);
728
- return false;
729
- }
730
- }
731
- return true;
732
- };
733
- const executeValidators = (validatorFunctions, element, validators) => {
734
- for (const validator in validators) {
735
- if (!validatorFunctions[validator]) {
736
- continue;
737
- }
738
- if (!executeValidator(validatorFunctions, validatorFunctions[validator], validators[validator], element)) {
739
- return false;
740
- }
741
- }
742
- return true;
743
- };
744
- const isMarkedForDestroy = element => {
745
- const elementName = element.name;
746
- const form = element.form;
747
- if (form && /\[([^\]]*?)\]$/.test(elementName)) {
748
- const destroyInputName = elementName.replace(/\[([^\]]*?)\]$/, '[_destroy]');
749
- const destroyInputElement = form.querySelector("input[name=\"".concat(destroyInputName, "\"]"));
750
- if (destroyInputElement && destroyInputElement.value === '1') {
751
- return true;
752
- }
753
- }
754
- return false;
755
- };
756
- const executeAllValidators = (element, validators) => {
757
- if (element.dataset.csvChanged === 'false' || element.disabled) {
758
- return;
759
- }
760
- element.dataset.csvChanged = 'false';
761
- if (executeValidators(ClientSideValidations.validators.all(), element, validators)) {
762
- passElement(element);
763
- }
764
- };
765
- const validateElement = (element, validators) => {
766
- dispatchCustomEvent(element, 'element:validate:before');
767
- if (isMarkedForDestroy(element)) {
768
- passElement(element);
769
- } else {
770
- executeAllValidators(element, validators);
771
- }
772
- return afterValidate(element);
773
- };
774
- if (!window.ClientSideValidations) {
775
- window.ClientSideValidations = ClientSideValidations;
776
- if (!isAMD() && !isCommonJS()) {
777
- ClientSideValidations.start();
778
- }
779
- }
780
- function isAMD() {
781
- return typeof define === 'function' && define.amd; // eslint-disable-line no-undef
782
- }
783
- function isCommonJS() {
784
- return typeof exports === 'object' && typeof module !== 'undefined';
785
- }
786
-
787
- return ClientSideValidations;
788
-
789
- }));
6
+ (function(global, factory) {
7
+ typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define([], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.ClientSideValidations = factory());
8
+ })(this, function() {
9
+ //#region src/events.js
10
+ const boundEventListeners = /* @__PURE__ */ new WeakMap();
11
+ const addBoundEventListener = (element, eventName, listener) => {
12
+ element.addEventListener(eventName, listener);
13
+ const listeners = boundEventListeners.get(element) || [];
14
+ listeners.push({
15
+ eventName,
16
+ listener
17
+ });
18
+ boundEventListeners.set(element, listeners);
19
+ };
20
+ const bindElementEvents = (element, eventsToBind) => {
21
+ for (const eventName in eventsToBind) addBoundEventListener(element, eventName, eventsToBind[eventName]);
22
+ };
23
+ const clearBoundEventListeners = (element) => {
24
+ const listeners = boundEventListeners.get(element);
25
+ if (!listeners) return;
26
+ listeners.forEach(({ eventName, listener }) => {
27
+ element.removeEventListener(eventName, listener);
28
+ });
29
+ boundEventListeners.delete(element);
30
+ };
31
+ const dispatchCustomEvent = (element, eventName, detail) => {
32
+ element.dispatchEvent(new CustomEvent(eventName, {
33
+ bubbles: true,
34
+ detail
35
+ }));
36
+ };
37
+ //#endregion
38
+ //#region src/utils.js
39
+ const arrayHasValue = (value, otherValues) => {
40
+ for (let i = 0, l = otherValues.length; i < l; i++) if (value === otherValues[i]) return true;
41
+ return false;
42
+ };
43
+ const createElementFromHTML = (html) => {
44
+ const element = document.createElement("div");
45
+ element.innerHTML = html;
46
+ return element.firstChild;
47
+ };
48
+ const isDOMCollection = (target) => {
49
+ return Array.isArray(target) || typeof NodeList !== "undefined" && target instanceof NodeList || typeof HTMLCollection !== "undefined" && target instanceof HTMLCollection || typeof RadioNodeList !== "undefined" && target instanceof RadioNodeList;
50
+ };
51
+ const isDOMElement = (target) => {
52
+ return target != null && target.nodeType === 1;
53
+ };
54
+ const getDOMElements = (target) => {
55
+ if (target == null) return [];
56
+ if (isDOMElement(target)) return [target];
57
+ if (isDOMCollection(target)) return Array.from(target).filter(isDOMElement);
58
+ return [];
59
+ };
60
+ const isFormElement = (element) => {
61
+ return element.tagName === "FORM";
62
+ };
63
+ const isInputElement = (element) => {
64
+ switch (element.tagName) {
65
+ case "INPUT": return element.type !== "submit" && element.type !== "button";
66
+ case "SELECT":
67
+ case "TEXTAREA": return true;
68
+ default: return false;
69
+ }
70
+ };
71
+ const isVisible = (element) => {
72
+ return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
73
+ };
74
+ const isValuePresent = (value) => {
75
+ return !/^\s*$/.test(value || "");
76
+ };
77
+ //#endregion
78
+ //#region src/core.js
79
+ const isNamedInputElement = (element) => {
80
+ return isInputElement(element) && element.name != null && element.name !== "";
81
+ };
82
+ const getFormControls = (form) => {
83
+ return Array.from(form.elements).filter(isInputElement);
84
+ };
85
+ const getFormInputs = (form) => {
86
+ return getFormControls(form).filter((element) => {
87
+ return isNamedInputElement(element) && !element.disabled && isVisible(element);
88
+ });
89
+ };
90
+ const findFormElementByName = (form, name) => {
91
+ return getFormControls(form).find((element) => element.name === name);
92
+ };
93
+ const enableForm = (form) => {
94
+ ClientSideValidations.enablers.form(form);
95
+ };
96
+ const enableForms = () => {
97
+ document.querySelectorAll(ClientSideValidations.selectors.forms).forEach(enableForm);
98
+ };
99
+ const ClientSideValidations = {
100
+ callbacks: {
101
+ element: {
102
+ after: (element, eventData) => {},
103
+ before: (element, eventData) => {},
104
+ fail: (element, message, addError, eventData) => addError(),
105
+ pass: (element, removeError, eventData) => removeError()
106
+ },
107
+ form: {
108
+ after: (form, eventData) => {},
109
+ before: (form, eventData) => {},
110
+ fail: (form, eventData) => {},
111
+ pass: (form, eventData) => {}
112
+ }
113
+ },
114
+ eventsToBind: {
115
+ form: (form) => ({
116
+ submit: (eventData) => {
117
+ if (!ClientSideValidations.isValid(form, form.ClientSideValidations.settings.validators)) {
118
+ eventData.preventDefault();
119
+ eventData.stopImmediatePropagation();
120
+ }
121
+ },
122
+ "ajax:beforeSend": function(eventData) {
123
+ if (eventData.target === this) ClientSideValidations.isValid(form, form.ClientSideValidations.settings.validators);
124
+ },
125
+ "form:validate:after": (eventData) => {
126
+ ClientSideValidations.callbacks.form.after(form, eventData);
127
+ },
128
+ "form:validate:before": (eventData) => {
129
+ ClientSideValidations.callbacks.form.before(form, eventData);
130
+ },
131
+ "form:validate:fail": (eventData) => {
132
+ ClientSideValidations.callbacks.form.fail(form, eventData);
133
+ },
134
+ "form:validate:pass": (eventData) => {
135
+ ClientSideValidations.callbacks.form.pass(form, eventData);
136
+ }
137
+ }),
138
+ input: (form) => ({
139
+ focusout: function() {
140
+ ClientSideValidations.isValid(this, form.ClientSideValidations.settings.validators);
141
+ },
142
+ change: function() {
143
+ this.dataset.csvChanged = "true";
144
+ },
145
+ "element:validate:after": function(eventData) {
146
+ ClientSideValidations.callbacks.element.after(this, eventData);
147
+ },
148
+ "element:validate:before": function(eventData) {
149
+ ClientSideValidations.callbacks.element.before(this, eventData);
150
+ },
151
+ "element:validate:fail": function(eventData) {
152
+ const element = this;
153
+ const message = eventData.detail;
154
+ ClientSideValidations.callbacks.element.fail(element, message, function() {
155
+ form.ClientSideValidations.addError(element, message);
156
+ }, eventData);
157
+ },
158
+ "element:validate:pass": function(eventData) {
159
+ const element = this;
160
+ ClientSideValidations.callbacks.element.pass(element, function() {
161
+ form.ClientSideValidations.removeError(element);
162
+ }, eventData);
163
+ }
164
+ }),
165
+ inputConfirmation: (elementToConfirm, form) => ({
166
+ focusout: () => {
167
+ elementToConfirm.dataset.csvChanged = "true";
168
+ ClientSideValidations.isValid(elementToConfirm, form.ClientSideValidations.settings.validators);
169
+ },
170
+ keyup: () => {
171
+ elementToConfirm.dataset.csvChanged = "true";
172
+ ClientSideValidations.isValid(elementToConfirm, form.ClientSideValidations.settings.validators);
173
+ }
174
+ })
175
+ },
176
+ enablers: {
177
+ form: (form) => {
178
+ clearBoundEventListeners(form);
179
+ getFormControls(form).forEach(clearBoundEventListeners);
180
+ form.ClientSideValidations = {
181
+ settings: JSON.parse(form.dataset.clientSideValidations),
182
+ addError: (element, message) => ClientSideValidations.formBuilders[form.ClientSideValidations.settings.html_settings.type].add(element, form.ClientSideValidations.settings.html_settings, message),
183
+ removeError: (element) => ClientSideValidations.formBuilders[form.ClientSideValidations.settings.html_settings.type].remove(element, form.ClientSideValidations.settings.html_settings)
184
+ };
185
+ bindElementEvents(form, ClientSideValidations.eventsToBind.form(form));
186
+ getFormInputs(form).forEach((element) => {
187
+ ClientSideValidations.enablers.input(element);
188
+ });
189
+ },
190
+ input: function(input) {
191
+ const form = input.form;
192
+ if (!form) return;
193
+ clearBoundEventListeners(input);
194
+ const eventsToBind = ClientSideValidations.eventsToBind.input(form);
195
+ if (input.type !== "radio" && !(input.id && input.id.endsWith("_confirmation"))) {
196
+ input.dataset.csvValidate = "true";
197
+ bindElementEvents(input, eventsToBind);
198
+ }
199
+ if (input.type === "checkbox") bindElementEvents(input, { change: function() {
200
+ ClientSideValidations.isValid(this, form.ClientSideValidations.settings.validators);
201
+ } });
202
+ if (input.id && input.id.endsWith("_confirmation")) {
203
+ const elementToConfirm = document.getElementById(input.id.match(/(.+)_confirmation/)[1]);
204
+ if (elementToConfirm && elementToConfirm.form === form) bindElementEvents(input, ClientSideValidations.eventsToBind.inputConfirmation(elementToConfirm, form));
205
+ }
206
+ }
207
+ },
208
+ formBuilders: { "ActionView::Helpers::FormBuilder": {
209
+ add: (element, settings, message) => {
210
+ if (!element) return;
211
+ const form = element.form;
212
+ const inputErrorTemplate = createElementFromHTML(settings.input_tag);
213
+ let inputErrorElement = element.closest(`.${inputErrorTemplate.getAttribute("class").replace(/ /g, ".")}`);
214
+ if (!inputErrorElement) {
215
+ inputErrorElement = inputErrorTemplate;
216
+ if (element.getAttribute("autofocus")) element.setAttribute("autofocus", false);
217
+ element.before(inputErrorElement);
218
+ inputErrorElement.querySelector("span#input_tag").replaceWith(element);
219
+ const inputErrorLabelMessageElement = inputErrorElement.querySelector("label.message");
220
+ if (inputErrorLabelMessageElement) inputErrorLabelMessageElement.setAttribute("for", element.id);
221
+ }
222
+ const labelElement = form.querySelector(`label[for="${element.id}"]:not(.message)`);
223
+ if (labelElement) {
224
+ const labelErrorTemplate = createElementFromHTML(settings.label_tag);
225
+ if (!labelElement.closest(`.${labelErrorTemplate.getAttribute("class").replace(/ /g, ".")}`)) {
226
+ labelElement.after(labelErrorTemplate);
227
+ labelErrorTemplate.querySelector("label#label_tag").replaceWith(labelElement);
228
+ }
229
+ }
230
+ const labelMessageElement = form.querySelector(`label.message[for="${element.id}"]`);
231
+ if (labelMessageElement) labelMessageElement.textContent = message;
232
+ },
233
+ remove: (element, settings) => {
234
+ if (!element) return;
235
+ const form = element.form;
236
+ const inputErrorClass = createElementFromHTML(settings.input_tag).getAttribute("class");
237
+ const inputErrorElement = element.closest(`.${inputErrorClass.replace(/ /g, ".")}`);
238
+ if (inputErrorElement) {
239
+ inputErrorElement.querySelector(`#${element.id}`).remove();
240
+ inputErrorElement.replaceWith(element);
241
+ }
242
+ const labelElement = form.querySelector(`label[for="${element.id}"]:not(.message)`);
243
+ if (labelElement) {
244
+ const labelErrorClass = createElementFromHTML(settings.label_tag).getAttribute("class");
245
+ const labelErrorElement = labelElement.closest(`.${labelErrorClass.replace(/ /g, ".")}`);
246
+ if (labelErrorElement) labelErrorElement.replaceWith(labelElement);
247
+ }
248
+ const labelMessageElement = form.querySelector(`label.message[for="${element.id}"]`);
249
+ if (labelMessageElement) labelMessageElement.remove();
250
+ }
251
+ } },
252
+ patterns: { numericality: {
253
+ default: /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/,
254
+ only_integer: /^[+-]?\d+$/
255
+ } },
256
+ selectors: { forms: "form[data-client-side-validations]" },
257
+ validators: {
258
+ all: () => {
259
+ return {
260
+ ...ClientSideValidations.validators.local,
261
+ ...ClientSideValidations.validators.remote
262
+ };
263
+ },
264
+ local: {},
265
+ remote: {}
266
+ },
267
+ disable: (target) => {
268
+ getDOMElements(target).forEach((element) => {
269
+ clearBoundEventListeners(element);
270
+ if (isFormElement(element)) {
271
+ getFormControls(element).forEach((input) => {
272
+ clearBoundEventListeners(input);
273
+ delete input.dataset.csvValid;
274
+ delete input.dataset.csvChanged;
275
+ delete input.dataset.csvValidate;
276
+ });
277
+ return;
278
+ }
279
+ delete element.dataset.csvValid;
280
+ delete element.dataset.csvChanged;
281
+ if (isInputElement(element)) delete element.dataset.csvValidate;
282
+ });
283
+ },
284
+ reset: (form) => {
285
+ ClientSideValidations.disable(form);
286
+ for (const key in form.ClientSideValidations.settings.validators) {
287
+ const element = findFormElementByName(form, key);
288
+ if (element) form.ClientSideValidations.removeError(element);
289
+ }
290
+ ClientSideValidations.enablers.form(form);
291
+ },
292
+ initializeOnEvent: () => {
293
+ if (window.Turbo != null) return "turbo:load";
294
+ else if (window.Turbolinks != null && window.Turbolinks.supported) return window.Turbolinks.EVENTS != null ? "page:change" : "turbolinks:load";
295
+ },
296
+ start: () => {
297
+ const initializeOnEvent = ClientSideValidations.initializeOnEvent();
298
+ if (initializeOnEvent != null) document.addEventListener(initializeOnEvent, enableForms);
299
+ else if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", enableForms, { once: true });
300
+ else enableForms();
301
+ }
302
+ };
303
+ //#endregion
304
+ //#region src/validators/local/absence_presence.js
305
+ const absenceLocalValidator = (element, options) => {
306
+ if (isValuePresent(element.value)) return options.message;
307
+ };
308
+ const presenceLocalValidator = (element, options) => {
309
+ if (!isValuePresent(element.value)) return options.message;
310
+ };
311
+ //#endregion
312
+ //#region src/validators/local/acceptance.js
313
+ const DEFAULT_ACCEPT_OPTION = ["1", true];
314
+ const isTextAccepted = (value, acceptOption) => {
315
+ if (!acceptOption) acceptOption = DEFAULT_ACCEPT_OPTION;
316
+ if (Array.isArray(acceptOption)) return arrayHasValue(value, acceptOption);
317
+ return value === acceptOption;
318
+ };
319
+ const acceptanceLocalValidator = (element, options) => {
320
+ let valid = true;
321
+ if (element.type === "checkbox") valid = element.checked;
322
+ if (element.type === "text") valid = isTextAccepted(element.value, options.accept);
323
+ if (!valid) return options.message;
324
+ };
325
+ //#endregion
326
+ //#region src/validators/local/format.js
327
+ const isMatching = (value, regExpOptions) => {
328
+ return new RegExp(regExpOptions.source, regExpOptions.options).test(value);
329
+ };
330
+ const hasValidFormat = (value, withOptions, withoutOptions) => {
331
+ return withOptions && isMatching(value, withOptions) || withoutOptions && !isMatching(value, withoutOptions);
332
+ };
333
+ const formatLocalValidator = (element, options) => {
334
+ const value = element.value;
335
+ if (options.allow_blank && !isValuePresent(value)) return;
336
+ if (!hasValidFormat(value, options.with, options.without)) return options.message;
337
+ };
338
+ //#endregion
339
+ //#region src/validators/local/numericality.js
340
+ const VALIDATIONS$1 = {
341
+ even: (a) => {
342
+ return parseInt(a, 10) % 2 === 0;
343
+ },
344
+ greater_than: (a, b) => {
345
+ return parseFloat(a) > parseFloat(b);
346
+ },
347
+ greater_than_or_equal_to: (a, b) => {
348
+ return parseFloat(a) >= parseFloat(b);
349
+ },
350
+ equal_to: (a, b) => {
351
+ return parseFloat(a) === parseFloat(b);
352
+ },
353
+ less_than: (a, b) => {
354
+ return parseFloat(a) < parseFloat(b);
355
+ },
356
+ less_than_or_equal_to: (a, b) => {
357
+ return parseFloat(a) <= parseFloat(b);
358
+ },
359
+ odd: (a) => {
360
+ return parseInt(a, 10) % 2 === 1;
361
+ },
362
+ other_than: (a, b) => {
363
+ return parseFloat(a) !== parseFloat(b);
364
+ }
365
+ };
366
+ const formatValue = (element) => {
367
+ const value = element.value || "";
368
+ const numberFormat = element.form.ClientSideValidations.settings.number_format;
369
+ return value.trim().replace(new RegExp(`\\${numberFormat.separator}`, "g"), ".");
370
+ };
371
+ const getOtherValue = (validationOption, form) => {
372
+ if (!isNaN(parseFloat(validationOption))) return validationOption;
373
+ const validationElements = form.querySelectorAll(`[name*="${validationOption}"]`);
374
+ if (validationElements.length === 1) {
375
+ const validationElement = validationElements[0];
376
+ const otherFormattedValue = formatValue(validationElement);
377
+ if (!isNaN(parseFloat(otherFormattedValue))) return otherFormattedValue;
378
+ }
379
+ };
380
+ const isValid = (validationFunction, validationOption, formattedValue, form) => {
381
+ if (validationFunction.length === 2) {
382
+ const otherValue = getOtherValue(validationOption, form);
383
+ return otherValue == null || otherValue === "" || validationFunction(formattedValue, otherValue);
384
+ } else return validationFunction(formattedValue);
385
+ };
386
+ const runFunctionValidations = (formattedValue, form, options) => {
387
+ for (const validation in VALIDATIONS$1) {
388
+ const validationOption = options[validation];
389
+ const validationFunction = VALIDATIONS$1[validation];
390
+ if (validationOption == null) continue;
391
+ if (!isValid(validationFunction, validationOption, formattedValue, form)) return options.messages[validation];
392
+ }
393
+ };
394
+ const runValidations$1 = (formattedValue, form, options) => {
395
+ if (options.only_integer && !ClientSideValidations.patterns.numericality.only_integer.test(formattedValue)) return options.messages.only_integer;
396
+ if (!ClientSideValidations.patterns.numericality.default.test(formattedValue)) return options.messages.numericality;
397
+ return runFunctionValidations(formattedValue, form, options);
398
+ };
399
+ const numericalityLocalValidator = (element, options) => {
400
+ const value = element.value;
401
+ if (options.allow_blank && !isValuePresent(value)) return;
402
+ const form = element.form;
403
+ const formattedValue = formatValue(element);
404
+ return runValidations$1(formattedValue, form, options);
405
+ };
406
+ //#endregion
407
+ //#region src/validators/local/length.js
408
+ const VALIDATIONS = {
409
+ is: (a, b) => {
410
+ return a === parseInt(b, 10);
411
+ },
412
+ minimum: (a, b) => {
413
+ return a >= parseInt(b, 10);
414
+ },
415
+ maximum: (a, b) => {
416
+ return a <= parseInt(b, 10);
417
+ }
418
+ };
419
+ const runValidations = (valueLength, options) => {
420
+ for (const validation in VALIDATIONS) {
421
+ const validationOption = options[validation];
422
+ const validationFunction = VALIDATIONS[validation];
423
+ if (validationOption && !validationFunction(valueLength, validationOption)) return options.messages[validation];
424
+ }
425
+ };
426
+ const lengthLocalValidator = (element, options) => {
427
+ const value = element.value;
428
+ if (options.allow_blank && !isValuePresent(value)) return;
429
+ return runValidations(value.length, options);
430
+ };
431
+ //#endregion
432
+ //#region src/validators/local/exclusion_inclusion.js
433
+ const isInList = (value, otherValues) => {
434
+ const normalizedOtherValues = [];
435
+ for (const otherValueIndex in otherValues) normalizedOtherValues.push(otherValues[otherValueIndex].toString());
436
+ return arrayHasValue(value, normalizedOtherValues);
437
+ };
438
+ const isInRange = (value, range) => {
439
+ return value >= range[0] && value <= range[1];
440
+ };
441
+ const isIncluded = (value, options, allowBlank) => {
442
+ if ((options.allow_blank && !isValuePresent(value)) === allowBlank) return true;
443
+ return options.in && isInList(value, options.in) || options.range && isInRange(value, options.range);
444
+ };
445
+ const exclusionLocalValidator = (element, options) => {
446
+ const value = element.value;
447
+ if (isIncluded(value, options, false) || !options.allow_blank && !isValuePresent(value)) return options.message;
448
+ };
449
+ const inclusionLocalValidator = (element, options) => {
450
+ const value = element.value;
451
+ if (!isIncluded(value, options, true)) return options.message;
452
+ };
453
+ //#endregion
454
+ //#region src/validators/local/confirmation.js
455
+ const confirmationLocalValidator = (element, options) => {
456
+ let value = element.value;
457
+ let confirmationValue = document.getElementById(`${element.id}_confirmation`).value;
458
+ if (!options.case_sensitive) {
459
+ value = value.toLowerCase();
460
+ confirmationValue = confirmationValue.toLowerCase();
461
+ }
462
+ if (value !== confirmationValue) return options.message;
463
+ };
464
+ //#endregion
465
+ //#region src/validators/local/uniqueness.js
466
+ const isLocallyUnique = (element, value, otherValue, caseSensitive) => {
467
+ if (!caseSensitive) {
468
+ value = value.toLowerCase();
469
+ otherValue = otherValue.toLowerCase();
470
+ }
471
+ if (otherValue === value) {
472
+ element.dataset.csvNotLocallyUnique = "true";
473
+ return false;
474
+ }
475
+ if (element.dataset.csvNotLocallyUnique) {
476
+ delete element.dataset.csvNotLocallyUnique;
477
+ element.dataset.csvChanged = "true";
478
+ }
479
+ return true;
480
+ };
481
+ const uniquenessLocalValidator = (element, options) => {
482
+ const elementName = element.name;
483
+ const matches = elementName.match(/^(.+_attributes\])\[\d+\](.+)$/);
484
+ if (!matches) return;
485
+ const form = element.form;
486
+ const value = element.value;
487
+ let valid = true;
488
+ const query = `[name^="${matches[1]}"][name$="${matches[2]}"]:not([name="${elementName}"])`;
489
+ const otherElements = form.querySelectorAll(query);
490
+ Array.prototype.slice.call(otherElements).forEach(function(otherElement) {
491
+ const otherValue = otherElement.value;
492
+ if (!isLocallyUnique(otherElement, value, otherValue, options.case_sensitive)) valid = false;
493
+ });
494
+ if (!valid) return options.message;
495
+ };
496
+ //#endregion
497
+ //#region src/index.js
498
+ ClientSideValidations.validators.local = {
499
+ absence: absenceLocalValidator,
500
+ presence: presenceLocalValidator,
501
+ acceptance: acceptanceLocalValidator,
502
+ format: formatLocalValidator,
503
+ numericality: numericalityLocalValidator,
504
+ length: lengthLocalValidator,
505
+ inclusion: inclusionLocalValidator,
506
+ exclusion: exclusionLocalValidator,
507
+ confirmation: confirmationLocalValidator,
508
+ uniqueness: uniquenessLocalValidator
509
+ };
510
+ ClientSideValidations.enable = (target) => {
511
+ getDOMElements(target).forEach((element) => {
512
+ if (isFormElement(element)) ClientSideValidations.enablers.form(element);
513
+ else if (isInputElement(element)) ClientSideValidations.enablers.input(element);
514
+ });
515
+ return target;
516
+ };
517
+ ClientSideValidations.validate = (target) => {
518
+ getDOMElements(target).forEach((element) => {
519
+ if (isFormElement(element)) ClientSideValidations.enable(element);
520
+ });
521
+ return target;
522
+ };
523
+ ClientSideValidations.isValid = (target, validators) => {
524
+ const element = getDOMElements(target)[0];
525
+ if (!element) return true;
526
+ if (!validators) validators = (isFormElement(element) ? element : element.form)?.ClientSideValidations?.settings?.validators;
527
+ if (isFormElement(element)) return validateForm(element, validators || {});
528
+ return validateElement(element, validatorsFor(element.name, validators || {}));
529
+ };
530
+ const cleanNestedElementName = (elementName, nestedMatches, validators) => {
531
+ for (const validatorName in validators) if (validatorName.match(`\\[${nestedMatches[1]}\\].*\\[\\]\\[${nestedMatches[2]}\\]$`)) elementName = elementName.replace(/\[[\da-z_]+\]\[(\w+)\]$/g, "[][$1]");
532
+ return elementName;
533
+ };
534
+ const cleanElementName = (elementName, validators) => {
535
+ elementName = elementName.replace(/\[(\w+_attributes)\]\[[\da-z_]+\](?=\[(?:\w+_attributes)\])/g, "[$1][]");
536
+ const nestedMatches = elementName.match(/\[(\w+_attributes)\].*\[(\w+)\]$/);
537
+ if (nestedMatches) elementName = cleanNestedElementName(elementName, nestedMatches, validators);
538
+ return elementName;
539
+ };
540
+ const validatorsFor = (elementName, validators) => {
541
+ if (!elementName || !validators) return {};
542
+ if (Object.prototype.hasOwnProperty.call(validators, elementName)) return validators[elementName];
543
+ return validators[cleanElementName(elementName, validators)] || {};
544
+ };
545
+ const getValidationInputs = (form) => {
546
+ return Array.from(form.elements).filter((element) => {
547
+ if (element.dataset.csvValidate == null || element.disabled) return false;
548
+ return isVisible(element);
549
+ });
550
+ };
551
+ const validateForm = (form, validators) => {
552
+ let valid = true;
553
+ dispatchCustomEvent(form, "form:validate:before");
554
+ getValidationInputs(form).forEach((element) => {
555
+ if (!validateElement(element, validatorsFor(element.name, validators))) valid = false;
556
+ });
557
+ if (valid) dispatchCustomEvent(form, "form:validate:pass");
558
+ else dispatchCustomEvent(form, "form:validate:fail");
559
+ dispatchCustomEvent(form, "form:validate:after");
560
+ return valid;
561
+ };
562
+ const passElement = (element) => {
563
+ dispatchCustomEvent(element, "element:validate:pass");
564
+ delete element.dataset.csvValid;
565
+ };
566
+ const failElement = (element, message) => {
567
+ dispatchCustomEvent(element, "element:validate:fail", message);
568
+ element.dataset.csvValid = "false";
569
+ };
570
+ const afterValidate = (element) => {
571
+ dispatchCustomEvent(element, "element:validate:after");
572
+ return element.dataset.csvValid !== "false";
573
+ };
574
+ const executeValidator = (validatorFunctions, validatorFunction, validatorOptions, element) => {
575
+ for (const validatorOption in validatorOptions) {
576
+ if (!validatorOptions[validatorOption]) continue;
577
+ const message = validatorFunction.call(validatorFunctions, element, validatorOptions[validatorOption]);
578
+ if (message) {
579
+ failElement(element, message);
580
+ return false;
581
+ }
582
+ }
583
+ return true;
584
+ };
585
+ const executeValidators = (validatorFunctions, element, validators) => {
586
+ for (const validator in validators) {
587
+ if (!validatorFunctions[validator]) continue;
588
+ if (!executeValidator(validatorFunctions, validatorFunctions[validator], validators[validator], element)) return false;
589
+ }
590
+ return true;
591
+ };
592
+ const isMarkedForDestroy = (element) => {
593
+ const elementName = element.name;
594
+ const form = element.form;
595
+ if (form && /\[([^\]]*?)\]$/.test(elementName)) {
596
+ const destroyInputName = elementName.replace(/\[([^\]]*?)\]$/, "[_destroy]");
597
+ const destroyInputElement = form.querySelector(`input[name="${destroyInputName}"]`);
598
+ if (destroyInputElement && destroyInputElement.value === "1") return true;
599
+ }
600
+ return false;
601
+ };
602
+ const executeAllValidators = (element, validators) => {
603
+ if (element.dataset.csvChanged === "false" || element.disabled) return;
604
+ element.dataset.csvChanged = "false";
605
+ if (executeValidators(ClientSideValidations.validators.all(), element, validators)) passElement(element);
606
+ };
607
+ const validateElement = (element, validators) => {
608
+ dispatchCustomEvent(element, "element:validate:before");
609
+ if (isMarkedForDestroy(element)) passElement(element);
610
+ else executeAllValidators(element, validators);
611
+ return afterValidate(element);
612
+ };
613
+ if (!window.ClientSideValidations) {
614
+ window.ClientSideValidations = ClientSideValidations;
615
+ if (!isAMD() && !isCommonJS()) ClientSideValidations.start();
616
+ }
617
+ function isAMD() {
618
+ return typeof define === "function" && define.amd;
619
+ }
620
+ function isCommonJS() {
621
+ return typeof exports === "object" && typeof module !== "undefined";
622
+ }
623
+ //#endregion
624
+ return ClientSideValidations;
625
+ });