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