form-father 0.2.11 → 0.4.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.
- package/CHANGELOG.md +98 -0
- package/README.md +110 -2
- package/demos/index.html +153 -0
- package/demos/main.js +103 -0
- package/demos/styles.css +221 -0
- package/dist/FormFather.min.js +1 -1
- package/dist/index.d.ts +183 -7
- package/dist/index.js +742 -129
- package/dist/index.js.map +1 -1
- package/dist/types/adapters.d.ts +28 -0
- package/dist/types/index.d.ts +153 -4
- package/dist/types/validators.d.ts +3 -2
- package/docs/en/README.md +225 -0
- package/docs/recipes/README.md +115 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -15,6 +15,18 @@ PERFORMANCE OF THIS SOFTWARE.
|
|
|
15
15
|
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
function __rest(s, e) {
|
|
19
|
+
var t = {};
|
|
20
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
21
|
+
t[p] = s[p];
|
|
22
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
23
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
24
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
25
|
+
t[p[i]] = s[p[i]];
|
|
26
|
+
}
|
|
27
|
+
return t;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
function __awaiter(thisArg, _arguments, P, generator) {
|
|
19
31
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
20
32
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -363,6 +375,65 @@ registerBuiltinValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный ф
|
|
|
363
375
|
registerBuiltinValidator('url', v => isUrlValid(v), 'Неверный формат');
|
|
364
376
|
registerBuiltinValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
|
|
365
377
|
|
|
378
|
+
function readFirstIssueMessage(error) {
|
|
379
|
+
var _a;
|
|
380
|
+
if (!error || typeof error !== 'object')
|
|
381
|
+
return undefined;
|
|
382
|
+
const issues = error.issues || error.errors;
|
|
383
|
+
if (Array.isArray(issues)) {
|
|
384
|
+
const firstMessage = (_a = issues.find(issue => typeof (issue === null || issue === void 0 ? void 0 : issue.message) === 'string')) === null || _a === void 0 ? void 0 : _a.message;
|
|
385
|
+
if (firstMessage)
|
|
386
|
+
return firstMessage;
|
|
387
|
+
}
|
|
388
|
+
return typeof error.message === 'string' ? error.message : undefined;
|
|
389
|
+
}
|
|
390
|
+
function resolveMessage(error, message) {
|
|
391
|
+
if (typeof message === 'function')
|
|
392
|
+
return message(error);
|
|
393
|
+
return message || readFirstIssueMessage(error);
|
|
394
|
+
}
|
|
395
|
+
function normalizeResult(result, message) {
|
|
396
|
+
if (typeof result === 'object' && result !== null)
|
|
397
|
+
return result;
|
|
398
|
+
if (result === undefined)
|
|
399
|
+
return true;
|
|
400
|
+
return result ? true : { valid: false, message };
|
|
401
|
+
}
|
|
402
|
+
function createSchemaValidator(schema, options = {}) {
|
|
403
|
+
return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
|
|
404
|
+
const mappedValue = options.mapValue ? options.mapValue(value, $input, $form, params) : value;
|
|
405
|
+
try {
|
|
406
|
+
if (typeof schema === 'function') {
|
|
407
|
+
return normalizeResult(yield schema(mappedValue), resolveMessage(undefined, options.message));
|
|
408
|
+
}
|
|
409
|
+
if (typeof schema.safeParse === 'function') {
|
|
410
|
+
const result = yield schema.safeParse(mappedValue);
|
|
411
|
+
if (result.success)
|
|
412
|
+
return true;
|
|
413
|
+
return { valid: false, message: resolveMessage(result.error || result, options.message) };
|
|
414
|
+
}
|
|
415
|
+
if (typeof schema.parse === 'function') {
|
|
416
|
+
yield schema.parse(mappedValue);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return { valid: false, message: resolveMessage(error, options.message) };
|
|
422
|
+
}
|
|
423
|
+
return { valid: false, message: resolveMessage(undefined, options.message) };
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function registerSchemaValidator(name, schema, defaultMessage, options = {}) {
|
|
427
|
+
const { override } = options, adapterOptions = __rest(options, ["override"]);
|
|
428
|
+
registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
|
|
429
|
+
}
|
|
430
|
+
function createFieldValidator(predicate, message) {
|
|
431
|
+
return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
|
|
432
|
+
const valid = yield predicate(value, $input, $form, params);
|
|
433
|
+
return valid ? true : { valid: false, message };
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
366
437
|
var _a;
|
|
367
438
|
// --- shared cross-file state (singleton via globalThis) ---
|
|
368
439
|
const FORM_GLOBAL_KEY = Symbol.for('formfather.shared');
|
|
@@ -374,7 +445,9 @@ const __shared =
|
|
|
374
445
|
defaultParams: {},
|
|
375
446
|
defaultValidationSchema: undefined,
|
|
376
447
|
});
|
|
448
|
+
const FORM_ERROR_FIELD = '_form';
|
|
377
449
|
let loaderIdCounter = 0;
|
|
450
|
+
const FORM_INSTANCES = new WeakMap();
|
|
378
451
|
/** Изначальная дефолтная схема. */
|
|
379
452
|
const INITIAL_DEFAULT_SCHEMA = {
|
|
380
453
|
tel: {
|
|
@@ -427,6 +500,11 @@ class Form {
|
|
|
427
500
|
this.$submits = [];
|
|
428
501
|
this.waitResponse = false;
|
|
429
502
|
this.inputs = null;
|
|
503
|
+
this.fieldEventHandlers = [];
|
|
504
|
+
this.liveValidationTimers = new WeakMap();
|
|
505
|
+
this.fieldValidationTokens = new WeakMap();
|
|
506
|
+
this.fieldValidationTokenCounter = 0;
|
|
507
|
+
this.errors = [];
|
|
430
508
|
/** Общие настройки анимаций (можно подправить под вкус) */
|
|
431
509
|
this._anim = {
|
|
432
510
|
duration: 220,
|
|
@@ -445,29 +523,27 @@ class Form {
|
|
|
445
523
|
loaderColor: '#fff',
|
|
446
524
|
showLoaderButton: true,
|
|
447
525
|
scrollToFirstErroredInput: true,
|
|
526
|
+
focusFirstErroredInput: false,
|
|
448
527
|
logging: false,
|
|
449
528
|
inputSelector: '.input',
|
|
450
529
|
inputWrapperSelector: '.input-primary',
|
|
530
|
+
validateOn: 'submit',
|
|
531
|
+
revalidateOn: ['input', 'change'],
|
|
532
|
+
validationDebounce: 0,
|
|
533
|
+
errorContainerAttribute: 'data-error-container',
|
|
534
|
+
validationStateAttribute: 'data-form-father-state',
|
|
535
|
+
observeMutations: false,
|
|
451
536
|
};
|
|
452
537
|
/* Слияние параметров: глобальные параметры → пользовательские параметры */
|
|
453
538
|
this.config = Object.assign({}, defaultConfig, Form.defaultParams, options);
|
|
454
539
|
if (this.$el) {
|
|
455
540
|
this.$submits = this.findSubmitElements();
|
|
456
|
-
this._onSubmitClickHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
457
|
-
var _a, _b, _c, _d;
|
|
458
|
-
e.preventDefault();
|
|
459
|
-
(_b = (_a = this.config).onBeforeValidate) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
460
|
-
const isValid = yield this.validate();
|
|
461
|
-
(_d = (_c = this.config).onAfterValidate) === null || _d === void 0 ? void 0 : _d.call(_c, isValid, this);
|
|
462
|
-
if (isValid) {
|
|
463
|
-
yield this.submit();
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
541
|
if (this.isCorrectArguments()) {
|
|
467
|
-
this
|
|
468
|
-
$submit.addEventListener('click', this._onSubmitClickHandler);
|
|
469
|
-
});
|
|
542
|
+
this.bindSubmitHandlers();
|
|
470
543
|
this.initialization();
|
|
544
|
+
this.bindFieldValidationEvents();
|
|
545
|
+
this.setupMutationObserver();
|
|
546
|
+
FORM_INSTANCES.set(this.$el, this);
|
|
471
547
|
}
|
|
472
548
|
}
|
|
473
549
|
else {
|
|
@@ -477,6 +553,193 @@ class Form {
|
|
|
477
553
|
getOptions() {
|
|
478
554
|
return this.options;
|
|
479
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Инициализирует сразу несколько форм по CSS-селектору. Если форма уже инициализирована, её настройки обновятся.
|
|
558
|
+
*
|
|
559
|
+
* @param selector - CSS-селектор форм. По умолчанию `form[data-form-father]`.
|
|
560
|
+
* @param options - Общие параметры для найденных форм.
|
|
561
|
+
*/
|
|
562
|
+
static initAll(selector = 'form[data-form-father]', options = {}) {
|
|
563
|
+
/* istanbul ignore next -- SSR guard: jsdom does not allow document to be unset cleanly. */
|
|
564
|
+
if (typeof document === 'undefined')
|
|
565
|
+
return [];
|
|
566
|
+
return Array.from(document.querySelectorAll(selector))
|
|
567
|
+
.filter($form => $form.tagName === 'FORM')
|
|
568
|
+
.map($form => {
|
|
569
|
+
const existing = FORM_INSTANCES.get($form);
|
|
570
|
+
if (existing) {
|
|
571
|
+
existing.updateOptions(options);
|
|
572
|
+
return existing;
|
|
573
|
+
}
|
|
574
|
+
return new Form($form, options);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/** Обновляет настройки конкретного инстанса формы без пересоздания обработчиков вручную. */
|
|
578
|
+
updateOptions(options) {
|
|
579
|
+
this.options = Object.assign(Object.assign({}, this.options), options);
|
|
580
|
+
this.config = Object.assign(Object.assign({}, this.config), options);
|
|
581
|
+
this.refreshControls();
|
|
582
|
+
this.bindSubmitHandlers();
|
|
583
|
+
this.bindFieldValidationEvents();
|
|
584
|
+
this.setupMutationObserver();
|
|
585
|
+
return this;
|
|
586
|
+
}
|
|
587
|
+
/** Возвращает текущий список ошибок формы. */
|
|
588
|
+
getErrors() {
|
|
589
|
+
return this.errors.map(error => (Object.assign({}, error)));
|
|
590
|
+
}
|
|
591
|
+
/** Возвращает значения формы обычным объектом. */
|
|
592
|
+
getValues() {
|
|
593
|
+
return serializeFormToJSON(this.$el);
|
|
594
|
+
}
|
|
595
|
+
normalizeValueList(value) {
|
|
596
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
597
|
+
}
|
|
598
|
+
/** Проставляет значения полей по `name` и уведомляет UI через `input`/`change` события. */
|
|
599
|
+
setValues(values) {
|
|
600
|
+
this.getAllFields().forEach($field => {
|
|
601
|
+
if (!$field.name || !Object.prototype.hasOwnProperty.call(values, $field.name))
|
|
602
|
+
return;
|
|
603
|
+
const value = values[$field.name];
|
|
604
|
+
if ($field instanceof HTMLInputElement && $field.type === 'file')
|
|
605
|
+
return;
|
|
606
|
+
if ($field instanceof HTMLInputElement && $field.type === 'checkbox') {
|
|
607
|
+
if (typeof value === 'boolean') {
|
|
608
|
+
$field.checked = value;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
$field.checked = this.normalizeValueList(value).includes($field.value);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
else if ($field instanceof HTMLInputElement && $field.type === 'radio') {
|
|
615
|
+
$field.checked = String(value) === $field.value;
|
|
616
|
+
}
|
|
617
|
+
else if ($field instanceof HTMLSelectElement && $field.multiple) {
|
|
618
|
+
const valuesList = this.normalizeValueList(value);
|
|
619
|
+
Array.from($field.options).forEach(option => {
|
|
620
|
+
option.selected = valuesList.includes(option.value);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
$field.value = value == null ? '' : String(value);
|
|
625
|
+
}
|
|
626
|
+
$field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
627
|
+
$field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
628
|
+
});
|
|
629
|
+
return this;
|
|
630
|
+
}
|
|
631
|
+
/** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
|
|
632
|
+
clearErrors() {
|
|
633
|
+
this.errors = [];
|
|
634
|
+
this.getAllFields().forEach($field => {
|
|
635
|
+
this.hideError($field);
|
|
636
|
+
this.clearFieldValidationState($field);
|
|
637
|
+
});
|
|
638
|
+
this.hideErrorForm();
|
|
639
|
+
return this;
|
|
640
|
+
}
|
|
641
|
+
isErrorResponse(issue) {
|
|
642
|
+
return (!!issue &&
|
|
643
|
+
typeof issue === 'object' &&
|
|
644
|
+
typeof issue.name === 'string' &&
|
|
645
|
+
typeof issue['error-msg'] === 'string');
|
|
646
|
+
}
|
|
647
|
+
normalizeValidationIssues(issues, source) {
|
|
648
|
+
if (issues == null || issues === true)
|
|
649
|
+
return [];
|
|
650
|
+
if (issues === false)
|
|
651
|
+
return [{ message: 'Некорректные данные формы', source }];
|
|
652
|
+
if (typeof issues === 'string')
|
|
653
|
+
return [{ message: issues, source }];
|
|
654
|
+
if (Array.isArray(issues)) {
|
|
655
|
+
return issues.flatMap(issue => this.normalizeValidationIssues(issue, source));
|
|
656
|
+
}
|
|
657
|
+
if (this.isErrorResponse(issues)) {
|
|
658
|
+
return [
|
|
659
|
+
{
|
|
660
|
+
field: issues.name,
|
|
661
|
+
rule: source,
|
|
662
|
+
message: issues['error-msg'],
|
|
663
|
+
source,
|
|
664
|
+
},
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
if (typeof issues === 'object' && 'message' in issues && typeof issues.message === 'string') {
|
|
668
|
+
return [Object.assign(Object.assign({}, issues), { source: issues.source || source })];
|
|
669
|
+
}
|
|
670
|
+
if (typeof issues === 'object') {
|
|
671
|
+
return Object.entries(issues).map(([field, message]) => ({
|
|
672
|
+
field,
|
|
673
|
+
rule: source,
|
|
674
|
+
message,
|
|
675
|
+
source,
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
return [];
|
|
679
|
+
}
|
|
680
|
+
applyValidationIssue(issue, source) {
|
|
681
|
+
const issueSource = issue.source || source;
|
|
682
|
+
const rule = issue.rule || issueSource;
|
|
683
|
+
const field = issue.field || FORM_ERROR_FIELD;
|
|
684
|
+
if (field === FORM_ERROR_FIELD) {
|
|
685
|
+
this.setErrorRecord(FORM_ERROR_FIELD, rule, issue.message, issueSource);
|
|
686
|
+
this.showErrorForm(issue.message);
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const $field = this.findFieldByName(field);
|
|
690
|
+
if (!$field) {
|
|
691
|
+
this.setErrorRecord(field, rule, issue.message, issueSource);
|
|
692
|
+
console.warn(`Не найдено поле с именем: ${field}, для вывода ошибки: ${issue.message}`);
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
this.showFieldError($field, issue.message, issueSource, rule);
|
|
696
|
+
return $field;
|
|
697
|
+
}
|
|
698
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
699
|
+
setErrors(issues, source = 'server') {
|
|
700
|
+
this.normalizeValidationIssues(issues, source).forEach(issue => {
|
|
701
|
+
this.applyValidationIssue(issue, source);
|
|
702
|
+
});
|
|
703
|
+
return this;
|
|
704
|
+
}
|
|
705
|
+
validateFormValidators() {
|
|
706
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
707
|
+
const validators = this.config.formValidators
|
|
708
|
+
? Array.isArray(this.config.formValidators)
|
|
709
|
+
? this.config.formValidators
|
|
710
|
+
: [this.config.formValidators]
|
|
711
|
+
: [];
|
|
712
|
+
const erroredInputs = [];
|
|
713
|
+
let ok = true;
|
|
714
|
+
for (const validator of validators) {
|
|
715
|
+
const result = yield validator({
|
|
716
|
+
form: this,
|
|
717
|
+
formElement: this.$el,
|
|
718
|
+
values: this.getValues(),
|
|
719
|
+
errors: this.getErrors(),
|
|
720
|
+
});
|
|
721
|
+
const issues = this.normalizeValidationIssues(result, 'client');
|
|
722
|
+
if (issues.length === 0)
|
|
723
|
+
continue;
|
|
724
|
+
ok = false;
|
|
725
|
+
issues.forEach(issue => {
|
|
726
|
+
const $field = this.applyValidationIssue(issue, 'client');
|
|
727
|
+
if ($field)
|
|
728
|
+
erroredInputs.push($field);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return { ok, erroredInputs };
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
735
|
+
reset(options = {}) {
|
|
736
|
+
this.$el.reset();
|
|
737
|
+
this.refreshControls();
|
|
738
|
+
if (options.clearErrors !== false) {
|
|
739
|
+
this.clearErrors();
|
|
740
|
+
}
|
|
741
|
+
return this;
|
|
742
|
+
}
|
|
480
743
|
/**
|
|
481
744
|
* Обновляет параметры по умолчанию для настроек формы. Метод объединяет переданные параметры с уже существующими
|
|
482
745
|
* параметрами по умолчанию.
|
|
@@ -487,21 +750,123 @@ class Form {
|
|
|
487
750
|
static setDefaultParams(params) {
|
|
488
751
|
this.defaultParams = Object.assign(Object.assign({}, this.defaultParams), params);
|
|
489
752
|
}
|
|
753
|
+
refreshControls() {
|
|
754
|
+
this.inputs = this.$el.querySelectorAll(this.config.inputSelector);
|
|
755
|
+
}
|
|
490
756
|
findSubmitElements() {
|
|
491
757
|
return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'));
|
|
492
758
|
}
|
|
759
|
+
bindSubmitHandlers() {
|
|
760
|
+
if (!this._onSubmitClickHandler) {
|
|
761
|
+
this._onSubmitClickHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
762
|
+
yield this.handleSubmitEvent(e);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
this.$submits.forEach($submit => {
|
|
766
|
+
$submit.removeEventListener('click', this._onSubmitClickHandler);
|
|
767
|
+
});
|
|
768
|
+
this.$submits = this.findSubmitElements();
|
|
769
|
+
this.$submits.forEach($submit => {
|
|
770
|
+
$submit.addEventListener('click', this._onSubmitClickHandler);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
493
773
|
initialization() {
|
|
494
|
-
this.
|
|
774
|
+
this.refreshControls();
|
|
495
775
|
this._onSubmitHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
496
|
-
|
|
776
|
+
yield this.handleSubmitEvent(e);
|
|
777
|
+
});
|
|
778
|
+
this.$el.addEventListener('submit', this._onSubmitHandler);
|
|
779
|
+
}
|
|
780
|
+
handleSubmitEvent(e) {
|
|
781
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
782
|
+
var _a, _b, _c, _d, _e, _f;
|
|
497
783
|
e.preventDefault();
|
|
498
784
|
(_b = (_a = this.config).onBeforeValidate) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
499
785
|
const isValid = yield this.validate();
|
|
500
786
|
(_d = (_c = this.config).onAfterValidate) === null || _d === void 0 ? void 0 : _d.call(_c, isValid, this);
|
|
501
|
-
if (isValid)
|
|
502
|
-
this.
|
|
787
|
+
if (!isValid) {
|
|
788
|
+
(_f = (_e = this.config).onValidationError) === null || _f === void 0 ? void 0 : _f.call(_e, this.getErrors(), this);
|
|
789
|
+
return {
|
|
790
|
+
success: false,
|
|
791
|
+
error: this.getErrors(),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return this.submit();
|
|
503
795
|
});
|
|
504
|
-
|
|
796
|
+
}
|
|
797
|
+
normalizeTriggers(value, fallback) {
|
|
798
|
+
const triggers = value == null ? fallback : Array.isArray(value) ? value : [value];
|
|
799
|
+
return Array.from(new Set(triggers));
|
|
800
|
+
}
|
|
801
|
+
clearFieldValidationEvents() {
|
|
802
|
+
this.fieldEventHandlers.forEach(({ $input, event, handler }) => {
|
|
803
|
+
$input.removeEventListener(event, handler);
|
|
804
|
+
});
|
|
805
|
+
this.fieldEventHandlers = [];
|
|
806
|
+
}
|
|
807
|
+
bindFieldValidationEvents() {
|
|
808
|
+
this.clearFieldValidationEvents();
|
|
809
|
+
const validateOn = this.normalizeTriggers(this.config.validateOn, ['submit']).filter((event) => event !== 'submit');
|
|
810
|
+
const revalidateOn = this.normalizeTriggers(this.config.revalidateOn, ['input', 'change']).filter((event) => event !== 'submit');
|
|
811
|
+
const events = Array.from(new Set([...validateOn, ...revalidateOn]));
|
|
812
|
+
if (events.length === 0)
|
|
813
|
+
return;
|
|
814
|
+
const fields = this.getAllFields();
|
|
815
|
+
fields.forEach($input => {
|
|
816
|
+
events.forEach(event => {
|
|
817
|
+
const handler = () => {
|
|
818
|
+
const shouldValidate = validateOn.includes(event) || this.hasFieldError($input);
|
|
819
|
+
if (shouldValidate)
|
|
820
|
+
this.scheduleFieldValidation($input);
|
|
821
|
+
};
|
|
822
|
+
$input.addEventListener(event, handler);
|
|
823
|
+
this.fieldEventHandlers.push({ $input, event, handler });
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
scheduleFieldValidation($input) {
|
|
828
|
+
const currentTimer = this.liveValidationTimers.get($input);
|
|
829
|
+
if (currentTimer)
|
|
830
|
+
clearTimeout(currentTimer);
|
|
831
|
+
const delay = Math.max(0, Number(this.config.validationDebounce) || 0);
|
|
832
|
+
const run = () => {
|
|
833
|
+
void this.validateField($input);
|
|
834
|
+
};
|
|
835
|
+
if (delay === 0) {
|
|
836
|
+
run();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const timer = setTimeout(run, delay);
|
|
840
|
+
this.liveValidationTimers.set($input, timer);
|
|
841
|
+
}
|
|
842
|
+
setupMutationObserver() {
|
|
843
|
+
var _a;
|
|
844
|
+
(_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
845
|
+
this.mutationObserver = undefined;
|
|
846
|
+
if (!this.config.observeMutations || typeof MutationObserver === 'undefined')
|
|
847
|
+
return;
|
|
848
|
+
this.mutationObserver = new MutationObserver(() => {
|
|
849
|
+
this.refreshControls();
|
|
850
|
+
this.bindSubmitHandlers();
|
|
851
|
+
this.bindFieldValidationEvents();
|
|
852
|
+
});
|
|
853
|
+
this.mutationObserver.observe(this.$el, {
|
|
854
|
+
childList: true,
|
|
855
|
+
subtree: true,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
getErrorContainer($input) {
|
|
859
|
+
const attrName = this.config.errorContainerAttribute || 'data-error-container';
|
|
860
|
+
const selector = $input.getAttribute(attrName);
|
|
861
|
+
if (!selector)
|
|
862
|
+
return null;
|
|
863
|
+
try {
|
|
864
|
+
return document.querySelector(selector);
|
|
865
|
+
}
|
|
866
|
+
catch (_a) {
|
|
867
|
+
console.warn(`[FormFather] Invalid error container selector "${selector}"`);
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
505
870
|
}
|
|
506
871
|
/**
|
|
507
872
|
* Показывает ошибку для поля ввода.
|
|
@@ -510,6 +875,15 @@ class Form {
|
|
|
510
875
|
* @param {String} text - Текст ошибки.
|
|
511
876
|
*/
|
|
512
877
|
showError($input, text) {
|
|
878
|
+
$input.setAttribute('aria-invalid', 'true');
|
|
879
|
+
const $errorContainer = this.getErrorContainer($input);
|
|
880
|
+
if ($errorContainer) {
|
|
881
|
+
$errorContainer.textContent = text;
|
|
882
|
+
if (!$errorContainer.hasAttribute('role'))
|
|
883
|
+
$errorContainer.setAttribute('role', 'alert');
|
|
884
|
+
$errorContainer.removeAttribute('hidden');
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
513
887
|
const $inputWrapper = closest($input, this.config.inputWrapperSelector);
|
|
514
888
|
if (!$inputWrapper) {
|
|
515
889
|
console.warn(`Не найдена обёртка для поля с именем: ${$input.name || $input.type}`);
|
|
@@ -520,7 +894,6 @@ class Form {
|
|
|
520
894
|
customShowError.call($inputWrapper, text);
|
|
521
895
|
return;
|
|
522
896
|
}
|
|
523
|
-
$input.setAttribute('aria-invalid', 'true');
|
|
524
897
|
$inputWrapper.classList.add('input__wrapper--error');
|
|
525
898
|
let $error = $inputWrapper.querySelector('[data-form-father-error]');
|
|
526
899
|
if (!$error) {
|
|
@@ -586,7 +959,12 @@ class Form {
|
|
|
586
959
|
hideError($input) {
|
|
587
960
|
var _a;
|
|
588
961
|
const $inputWrapper = closest($input, this.config.inputWrapperSelector) || $input.parentElement;
|
|
962
|
+
const $errorContainer = this.getErrorContainer($input);
|
|
589
963
|
$input.removeAttribute('aria-invalid');
|
|
964
|
+
if ($errorContainer) {
|
|
965
|
+
$errorContainer.textContent = '';
|
|
966
|
+
$errorContainer.setAttribute('hidden', '');
|
|
967
|
+
}
|
|
590
968
|
(_a = $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.querySelector('[data-form-father-error]')) === null || _a === void 0 ? void 0 : _a.remove();
|
|
591
969
|
$inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.classList.remove('input__wrapper--error');
|
|
592
970
|
if ($inputWrapper && $inputWrapper.classList.contains('input__wrapper')) {
|
|
@@ -600,7 +978,7 @@ class Form {
|
|
|
600
978
|
*/
|
|
601
979
|
scrollToFirstErroredInput(inputsList) {
|
|
602
980
|
if (inputsList.length > 0) {
|
|
603
|
-
const allInputInFormList = Array.from(this.$el.querySelectorAll('input'));
|
|
981
|
+
const allInputInFormList = Array.from(this.$el.querySelectorAll('input, textarea, select'));
|
|
604
982
|
let $firstInputOnForm = null;
|
|
605
983
|
let minIndex = Infinity;
|
|
606
984
|
inputsList.forEach($input => {
|
|
@@ -625,125 +1003,333 @@ class Form {
|
|
|
625
1003
|
}
|
|
626
1004
|
}
|
|
627
1005
|
}
|
|
1006
|
+
focusFirstErroredInput(inputsList) {
|
|
1007
|
+
var _a;
|
|
1008
|
+
if (inputsList.length === 0)
|
|
1009
|
+
return;
|
|
1010
|
+
const allInputInFormList = Array.from(this.$el.querySelectorAll('input, textarea, select'));
|
|
1011
|
+
const $firstInputOnForm = inputsList
|
|
1012
|
+
.slice()
|
|
1013
|
+
.sort((a, b) => allInputInFormList.indexOf(a) - allInputInFormList.indexOf(b))[0];
|
|
1014
|
+
(_a = $firstInputOnForm === null || $firstInputOnForm === void 0 ? void 0 : $firstInputOnForm.focus) === null || _a === void 0 ? void 0 : _a.call($firstInputOnForm, { preventScroll: true });
|
|
1015
|
+
}
|
|
1016
|
+
getValidationSchema() {
|
|
1017
|
+
return Object.assign(Object.assign({}, (Form.defaultValidationSchema || {})), (this.config.validationSchema || {}));
|
|
1018
|
+
}
|
|
1019
|
+
getAllFields($block = this.$el) {
|
|
1020
|
+
return Array.from($block.querySelectorAll('input, textarea, select'));
|
|
1021
|
+
}
|
|
1022
|
+
getFieldKey($input) {
|
|
1023
|
+
return $input.name || $input.id || $input.getAttribute('data-field') || $input.type || 'field';
|
|
1024
|
+
}
|
|
1025
|
+
hasFieldError($input) {
|
|
1026
|
+
const field = this.getFieldKey($input);
|
|
1027
|
+
return $input.getAttribute('aria-invalid') === 'true' || this.errors.some(error => error.field === field);
|
|
1028
|
+
}
|
|
1029
|
+
clearFieldErrorRecord($input) {
|
|
1030
|
+
const field = this.getFieldKey($input);
|
|
1031
|
+
this.errors = this.errors.filter(error => error.field !== field);
|
|
1032
|
+
}
|
|
1033
|
+
setErrorRecord(field, rule, message, source) {
|
|
1034
|
+
this.errors.push({
|
|
1035
|
+
field,
|
|
1036
|
+
rule,
|
|
1037
|
+
message,
|
|
1038
|
+
source,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
getValidationStateAttribute() {
|
|
1042
|
+
return this.config.validationStateAttribute || 'data-form-father-state';
|
|
1043
|
+
}
|
|
1044
|
+
setFieldValidationState($input, state) {
|
|
1045
|
+
const attr = this.getValidationStateAttribute();
|
|
1046
|
+
if (state) {
|
|
1047
|
+
$input.setAttribute(attr, state);
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
$input.removeAttribute(attr);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
startFieldValidation($input) {
|
|
1054
|
+
const token = ++this.fieldValidationTokenCounter;
|
|
1055
|
+
this.fieldValidationTokens.set($input, token);
|
|
1056
|
+
$input.setAttribute('aria-busy', 'true');
|
|
1057
|
+
this.setFieldValidationState($input, 'validating');
|
|
1058
|
+
return token;
|
|
1059
|
+
}
|
|
1060
|
+
isLatestFieldValidation($input, token) {
|
|
1061
|
+
return token == null || this.fieldValidationTokens.get($input) === token;
|
|
1062
|
+
}
|
|
1063
|
+
finishFieldValidation($input, token, isValid) {
|
|
1064
|
+
if (!this.isLatestFieldValidation($input, token))
|
|
1065
|
+
return;
|
|
1066
|
+
$input.removeAttribute('aria-busy');
|
|
1067
|
+
this.setFieldValidationState($input, isValid ? 'valid' : 'invalid');
|
|
1068
|
+
}
|
|
1069
|
+
clearFieldValidationState($input) {
|
|
1070
|
+
this.fieldValidationTokens.delete($input);
|
|
1071
|
+
$input.removeAttribute('aria-busy');
|
|
1072
|
+
this.setFieldValidationState($input, null);
|
|
1073
|
+
}
|
|
1074
|
+
setValidationError($input, rule, message, source) {
|
|
1075
|
+
this.clearFieldErrorRecord($input);
|
|
1076
|
+
this.setErrorRecord(this.getFieldKey($input), rule, message, source);
|
|
1077
|
+
}
|
|
1078
|
+
resolveField(field) {
|
|
1079
|
+
if (typeof field !== 'string')
|
|
1080
|
+
return field;
|
|
1081
|
+
return this.findFieldByName(field);
|
|
1082
|
+
}
|
|
1083
|
+
parseRuleString(ruleString) {
|
|
1084
|
+
const [rule, ...params] = ruleString.split(':').map(s => s.trim());
|
|
1085
|
+
return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
|
|
1086
|
+
}
|
|
1087
|
+
parseValidationAttribute(value) {
|
|
1088
|
+
return (value !== null && value !== void 0 ? value : '')
|
|
1089
|
+
.split(/[,\s|]+/)
|
|
1090
|
+
.map(s => s.trim())
|
|
1091
|
+
.filter(Boolean)
|
|
1092
|
+
.map(ruleString => this.parseRuleString(ruleString));
|
|
1093
|
+
}
|
|
1094
|
+
getRuleName(rule) {
|
|
1095
|
+
return typeof rule === 'string' ? rule : rule.rule;
|
|
1096
|
+
}
|
|
1097
|
+
getRuleIdentity(rule) {
|
|
1098
|
+
return JSON.stringify(rule);
|
|
1099
|
+
}
|
|
1100
|
+
collectRules($input, fromSchema = [], schemaKeys = new Set()) {
|
|
1101
|
+
const list = [];
|
|
1102
|
+
if ($input.hasAttribute('required'))
|
|
1103
|
+
list.push('required');
|
|
1104
|
+
list.push(...fromSchema);
|
|
1105
|
+
const dataValidateRules = this.parseValidationAttribute($input.getAttribute('data-validate')).filter(rule => {
|
|
1106
|
+
const ruleName = this.getRuleName(rule);
|
|
1107
|
+
return getValidator(ruleName) || !schemaKeys.has(ruleName);
|
|
1108
|
+
});
|
|
1109
|
+
list.push(...dataValidateRules);
|
|
1110
|
+
list.push(...this.parseValidationAttribute($input.getAttribute('data-custom-validate')));
|
|
1111
|
+
const filtered = [];
|
|
1112
|
+
const seen = new Set();
|
|
1113
|
+
for (const rule of list) {
|
|
1114
|
+
const ruleName = this.getRuleName(rule);
|
|
1115
|
+
if (!getValidator(ruleName)) {
|
|
1116
|
+
console.warn(`[FormFather] Unknown validation rule "${ruleName}"`);
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const identity = this.getRuleIdentity(rule);
|
|
1120
|
+
if (seen.has(identity))
|
|
1121
|
+
continue;
|
|
1122
|
+
seen.add(identity);
|
|
1123
|
+
filtered.push(rule);
|
|
1124
|
+
}
|
|
1125
|
+
return filtered;
|
|
1126
|
+
}
|
|
1127
|
+
getRuleMessage($input, rule, messages, defaultMessage, overrideMessage) {
|
|
1128
|
+
const attrMessage = $input.getAttribute(`data-error-${rule}`);
|
|
1129
|
+
return overrideMessage || attrMessage || messages[rule] || defaultMessage;
|
|
1130
|
+
}
|
|
1131
|
+
fieldMatchesSchemaDefinition($input, key, def) {
|
|
1132
|
+
if (def.selector) {
|
|
1133
|
+
try {
|
|
1134
|
+
return $input.matches(def.selector);
|
|
1135
|
+
}
|
|
1136
|
+
catch (_a) {
|
|
1137
|
+
console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return this.parseValidationAttribute($input.getAttribute('data-validate')).some(rule => this.getRuleName(rule) === key);
|
|
1142
|
+
}
|
|
1143
|
+
getFieldValidationConfig($input, schema = this.getValidationSchema()) {
|
|
1144
|
+
const rules = [];
|
|
1145
|
+
const messages = {};
|
|
1146
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
1147
|
+
if (!this.fieldMatchesSchemaDefinition($input, key, def))
|
|
1148
|
+
continue;
|
|
1149
|
+
rules.push(...def.rules);
|
|
1150
|
+
Object.assign(messages, def.messages || {});
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
rules: this.collectRules($input, rules, new Set(Object.keys(schema))),
|
|
1154
|
+
messages,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
getValidationFields($block, schema) {
|
|
1158
|
+
const fields = new Set();
|
|
1159
|
+
const addFields = (nodeList) => {
|
|
1160
|
+
Array.from(nodeList).forEach($input => {
|
|
1161
|
+
if ($input instanceof HTMLInputElement ||
|
|
1162
|
+
$input instanceof HTMLTextAreaElement ||
|
|
1163
|
+
$input instanceof HTMLSelectElement) {
|
|
1164
|
+
fields.add($input);
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
};
|
|
1168
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
1169
|
+
if (def.selector) {
|
|
1170
|
+
try {
|
|
1171
|
+
addFields($block.querySelectorAll(def.selector));
|
|
1172
|
+
}
|
|
1173
|
+
catch (_a) {
|
|
1174
|
+
console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
|
|
1175
|
+
}
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
this.getAllFields($block)
|
|
1179
|
+
.filter($input => this.parseValidationAttribute($input.getAttribute('data-validate')).some(rule => this.getRuleName(rule) === key))
|
|
1180
|
+
.forEach($input => fields.add($input));
|
|
1181
|
+
}
|
|
1182
|
+
addFields($block.querySelectorAll('[required], [data-validate], [data-custom-validate]'));
|
|
1183
|
+
return Array.from(fields);
|
|
1184
|
+
}
|
|
1185
|
+
validateInput($input_1, rules_1) {
|
|
1186
|
+
return __awaiter(this, arguments, void 0, function* ($input, rules, messages = {}, $block = this.$el, validationToken) {
|
|
1187
|
+
if ($input.hasAttribute('data-no-validate'))
|
|
1188
|
+
return true;
|
|
1189
|
+
const isCheckable = $input instanceof HTMLInputElement && ($input.type === 'checkbox' || $input.type === 'radio');
|
|
1190
|
+
const empty = isCheckable ? !$input.checked : $input.value.trim().length === 0;
|
|
1191
|
+
const hasRequiredRule = rules.some(rule => this.getRuleName(rule) === 'required');
|
|
1192
|
+
if (empty && !hasRequiredRule) {
|
|
1193
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1194
|
+
return !this.hasFieldError($input);
|
|
1195
|
+
this.clearFieldErrorRecord($input);
|
|
1196
|
+
this.hideError($input);
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
for (const validationRule of rules) {
|
|
1200
|
+
const { rule, params } = typeof validationRule === 'string'
|
|
1201
|
+
? { rule: validationRule, params: undefined }
|
|
1202
|
+
: { rule: validationRule.rule, params: validationRule.params };
|
|
1203
|
+
const validator = getValidator(rule);
|
|
1204
|
+
if (!validator)
|
|
1205
|
+
continue;
|
|
1206
|
+
const raw = yield validator.fn($input.value, $input, $block, params);
|
|
1207
|
+
const isObjectResult = typeof raw === 'object' && raw !== null;
|
|
1208
|
+
const passed = isObjectResult ? raw.valid === true : !!raw;
|
|
1209
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1210
|
+
return !this.hasFieldError($input);
|
|
1211
|
+
if (isObjectResult && raw.effect) {
|
|
1212
|
+
yield raw.effect({
|
|
1213
|
+
value: $input.value,
|
|
1214
|
+
$input,
|
|
1215
|
+
$form: $block,
|
|
1216
|
+
params,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1220
|
+
return !this.hasFieldError($input);
|
|
1221
|
+
if (!passed) {
|
|
1222
|
+
const result = raw;
|
|
1223
|
+
const message = this.getRuleMessage($input, rule, messages, validator.defaultMessage, isObjectResult && typeof result.message === 'string' ? result.message : undefined);
|
|
1224
|
+
this.setValidationError($input, rule, message, 'client');
|
|
1225
|
+
this.showError($input, message);
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
if (isObjectResult && raw.stopOthers) {
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1233
|
+
return !this.hasFieldError($input);
|
|
1234
|
+
this.clearFieldErrorRecord($input);
|
|
1235
|
+
this.hideError($input);
|
|
1236
|
+
return true;
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
runFieldValidation($input_1, rules_1) {
|
|
1240
|
+
return __awaiter(this, arguments, void 0, function* ($input, rules, messages = {}, $block = this.$el) {
|
|
1241
|
+
const validationToken = this.startFieldValidation($input);
|
|
1242
|
+
try {
|
|
1243
|
+
const isValid = yield this.validateInput($input, rules, messages, $block, validationToken);
|
|
1244
|
+
this.finishFieldValidation($input, validationToken, isValid);
|
|
1245
|
+
return isValid;
|
|
1246
|
+
}
|
|
1247
|
+
catch (error) {
|
|
1248
|
+
if (this.isLatestFieldValidation($input, validationToken)) {
|
|
1249
|
+
this.clearFieldValidationState($input);
|
|
1250
|
+
}
|
|
1251
|
+
throw error;
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
getRadioGroup($input, $block) {
|
|
1256
|
+
if (!($input instanceof HTMLInputElement) || $input.type !== 'radio' || !$input.name)
|
|
1257
|
+
return [];
|
|
1258
|
+
return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
|
|
1259
|
+
}
|
|
1260
|
+
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
1261
|
+
showFieldError(field, message, source = 'manual', rule = source) {
|
|
1262
|
+
const $field = this.resolveField(field);
|
|
1263
|
+
if (!$field) {
|
|
1264
|
+
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
this.setValidationError($field, rule, message, source);
|
|
1268
|
+
this.showError($field, message);
|
|
1269
|
+
$field.removeAttribute('aria-busy');
|
|
1270
|
+
this.setFieldValidationState($field, 'invalid');
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
1274
|
+
validateField(field) {
|
|
1275
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1276
|
+
const $field = this.resolveField(field);
|
|
1277
|
+
if (!$field) {
|
|
1278
|
+
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
const { rules, messages } = this.getFieldValidationConfig($field);
|
|
1282
|
+
return this.runFieldValidation($field, rules, messages);
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
628
1285
|
/**
|
|
629
1286
|
* Проверяет все поля ввода.
|
|
630
1287
|
*
|
|
631
|
-
* Порядок для
|
|
1288
|
+
* Порядок для каждого поля:
|
|
632
1289
|
*
|
|
633
1290
|
* 1. `required` — если атрибут `required`
|
|
634
1291
|
* 2. правила из `validationSchema`
|
|
635
|
-
* 3. правила из `data-
|
|
1292
|
+
* 3. правила из `data-validate`
|
|
1293
|
+
* 4. правила из `data-custom-validate`
|
|
636
1294
|
*
|
|
637
1295
|
* Для одного поля показывается только первая ошибка. Неизвестные правила фиксируются `console.warn` и исключаются ДО
|
|
638
|
-
*
|
|
1296
|
+
* валидации.
|
|
639
1297
|
*/
|
|
640
1298
|
validate() {
|
|
641
1299
|
return __awaiter(this, arguments, void 0, function* ($block = this.$el) {
|
|
642
|
-
|
|
643
|
-
const schema = Object.assign(Object.assign({}, (Form.defaultValidationSchema || {})), (this.config.validationSchema || {}));
|
|
644
|
-
const warnUnknown = (r) => console.warn(`[FormFather] Unknown validation rule "${r}"`);
|
|
1300
|
+
const schema = this.getValidationSchema();
|
|
645
1301
|
const erroredInputs = [];
|
|
646
1302
|
const processed = new WeakSet();
|
|
647
1303
|
let ok = true;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
list.push(...fromSchema); // 2
|
|
656
|
-
const custom = ((_a = $input.getAttribute('data-custom-validate')) !== null && _a !== void 0 ? _a : '')
|
|
657
|
-
.split(',')
|
|
658
|
-
.map(s => s.trim())
|
|
659
|
-
.filter(Boolean);
|
|
660
|
-
list.push(...custom.map(ruleString => {
|
|
661
|
-
const [rule, ...params] = ruleString.split(':').map(s => s.trim());
|
|
662
|
-
return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
|
|
663
|
-
})); // 3
|
|
664
|
-
/* —–– предварительная фильтрация unknown ––– */
|
|
665
|
-
const filtered = [];
|
|
666
|
-
for (const r of list) {
|
|
667
|
-
const ruleName = typeof r === 'string' ? r : r.rule;
|
|
668
|
-
if (getValidator(ruleName))
|
|
669
|
-
filtered.push(r);
|
|
670
|
-
else
|
|
671
|
-
warnUnknown(ruleName);
|
|
1304
|
+
this.errors = [];
|
|
1305
|
+
for (const $input of this.getValidationFields($block, schema)) {
|
|
1306
|
+
if (processed.has($input))
|
|
1307
|
+
continue;
|
|
1308
|
+
const radioGroup = this.getRadioGroup($input, $block);
|
|
1309
|
+
if (radioGroup.length > 0) {
|
|
1310
|
+
radioGroup.forEach($radio => processed.add($radio));
|
|
672
1311
|
}
|
|
673
|
-
|
|
674
|
-
};
|
|
675
|
-
const validateInput = ($input_1, rules_1, ...args_1) => __awaiter(this, [$input_1, rules_1, ...args_1], void 0, function* ($input, rules, msgs = {}) {
|
|
676
|
-
if ($input.hasAttribute('data-no-validate'))
|
|
677
|
-
return;
|
|
678
|
-
const empty = $input.type === 'checkbox' || $input.type === 'radio' ? !$input.checked : $input.value.trim().length === 0;
|
|
679
|
-
const hasRequiredRule = rules.some(r => (typeof r === 'string' ? r === 'required' : r.rule === 'required'));
|
|
680
|
-
if (empty && !hasRequiredRule) {
|
|
681
|
-
this.hideError($input);
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
for (const r of rules) {
|
|
685
|
-
const { rule, params } = typeof r === 'string' ? { rule: r, params: undefined } : r;
|
|
686
|
-
const vd = getValidator(rule); // гарантированно существует
|
|
687
|
-
const raw = yield vd.fn($input.value, $input, $block, params);
|
|
688
|
-
const isObj = typeof raw === 'object' && raw !== null;
|
|
689
|
-
const passed = isObj ? raw.valid === true : !!raw;
|
|
690
|
-
// сайд-эффект (если есть)
|
|
691
|
-
if (isObj && raw.effect) {
|
|
692
|
-
yield raw.effect({
|
|
693
|
-
value: $input.value,
|
|
694
|
-
$input,
|
|
695
|
-
$form: $block,
|
|
696
|
-
params,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
if (!passed) {
|
|
700
|
-
ok = false;
|
|
701
|
-
const r = raw;
|
|
702
|
-
const msg = (isObj && typeof r.message === 'string' && r.message) || msgs[rule] || vd.defaultMessage;
|
|
703
|
-
this.showError($input, msg);
|
|
704
|
-
erroredInputs.push($input);
|
|
705
|
-
return; // первую ошибку отобразили
|
|
706
|
-
}
|
|
707
|
-
// хотим прервать цепочку правил, даже если успех
|
|
708
|
-
if (isObj && raw.stopOthers) {
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
this.hideError($input);
|
|
713
|
-
});
|
|
714
|
-
const getRadioGroup = ($input) => {
|
|
715
|
-
if ($input.type !== 'radio' || !$input.name)
|
|
716
|
-
return [];
|
|
717
|
-
return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
|
|
718
|
-
};
|
|
719
|
-
/* ---------- 1. inputs из schema (config + defaults) ---------- */
|
|
720
|
-
for (const [key, def] of Object.entries(schema)) {
|
|
721
|
-
const { selector, rules = [], messages = {} } = def;
|
|
722
|
-
const nodeList = selector
|
|
723
|
-
? $block.querySelectorAll(selector) // явный CSS-селектор
|
|
724
|
-
: $block.querySelectorAll(`[data-validate="${key}"]`); // fallback по name
|
|
725
|
-
for (const $input of Array.from(nodeList)) {
|
|
726
|
-
if (processed.has($input))
|
|
727
|
-
continue;
|
|
728
|
-
const radioGroup = getRadioGroup($input);
|
|
729
|
-
if (radioGroup.length > 0) {
|
|
730
|
-
radioGroup.forEach($radio => processed.add($radio));
|
|
731
|
-
yield validateInput($input, collectRules($input, rules), messages);
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
1312
|
+
else {
|
|
734
1313
|
processed.add($input);
|
|
735
|
-
|
|
1314
|
+
}
|
|
1315
|
+
const { rules, messages } = this.getFieldValidationConfig($input, schema);
|
|
1316
|
+
const fieldValid = yield this.runFieldValidation($input, rules, messages, $block);
|
|
1317
|
+
if (!fieldValid) {
|
|
1318
|
+
ok = false;
|
|
1319
|
+
erroredInputs.push($input);
|
|
736
1320
|
}
|
|
737
1321
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
yield validateInput($input, collectRules($input, []));
|
|
1322
|
+
const formValidation = yield this.validateFormValidators();
|
|
1323
|
+
if (!formValidation.ok) {
|
|
1324
|
+
ok = false;
|
|
1325
|
+
erroredInputs.push(...formValidation.erroredInputs);
|
|
743
1326
|
}
|
|
744
|
-
|
|
1327
|
+
const visibleErroredInputs = erroredInputs.filter(item => !item.hasAttribute('data-no-error-scroll'));
|
|
745
1328
|
if (!ok && this.config.scrollToFirstErroredInput) {
|
|
746
|
-
this.scrollToFirstErroredInput(
|
|
1329
|
+
this.scrollToFirstErroredInput(visibleErroredInputs);
|
|
1330
|
+
}
|
|
1331
|
+
if (!ok && this.config.focusFirstErroredInput) {
|
|
1332
|
+
this.focusFirstErroredInput(visibleErroredInputs);
|
|
747
1333
|
}
|
|
748
1334
|
return ok;
|
|
749
1335
|
});
|
|
@@ -949,6 +1535,7 @@ class Form {
|
|
|
949
1535
|
}
|
|
950
1536
|
/** Полностью снимает слушатели и чистит артефакты формы. */
|
|
951
1537
|
destroy() {
|
|
1538
|
+
var _a;
|
|
952
1539
|
// 1) Снять submit-слушатель
|
|
953
1540
|
if (this._onSubmitHandler) {
|
|
954
1541
|
this.$el.removeEventListener('submit', this._onSubmitHandler);
|
|
@@ -961,6 +1548,9 @@ class Form {
|
|
|
961
1548
|
});
|
|
962
1549
|
this._onSubmitClickHandler = undefined;
|
|
963
1550
|
}
|
|
1551
|
+
this.clearFieldValidationEvents();
|
|
1552
|
+
(_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
1553
|
+
this.mutationObserver = undefined;
|
|
964
1554
|
// 2) Удалить формовый error-блок под инпутами (если есть)
|
|
965
1555
|
const formErrorWrapper = this.$el.querySelector('.error-block-under-input__wrapper');
|
|
966
1556
|
if (formErrorWrapper) {
|
|
@@ -976,13 +1566,18 @@ class Form {
|
|
|
976
1566
|
});
|
|
977
1567
|
// 4) Сбросить ошибки у инпутов (если разметка позволяет)
|
|
978
1568
|
const inputs = this.$el.querySelectorAll('input, textarea, select');
|
|
979
|
-
inputs.forEach($i =>
|
|
1569
|
+
inputs.forEach($i => {
|
|
1570
|
+
this.hideError($i);
|
|
1571
|
+
this.clearFieldValidationState($i);
|
|
1572
|
+
});
|
|
980
1573
|
// 5) Убрать возможный флаг скролла
|
|
981
1574
|
document.documentElement.removeAttribute('data-now-scrolling');
|
|
982
1575
|
// 6) Очистить ссылки на элементы, чтобы помочь GC
|
|
983
1576
|
this.inputs = null;
|
|
984
1577
|
this.$submits = [];
|
|
1578
|
+
this.errors = [];
|
|
985
1579
|
this.waitResponse = false;
|
|
1580
|
+
FORM_INSTANCES.delete(this.$el);
|
|
986
1581
|
}
|
|
987
1582
|
/**
|
|
988
1583
|
* Проверяет входные данные на их корректность.
|
|
@@ -1099,7 +1694,10 @@ class Form {
|
|
|
1099
1694
|
const $field = this.findFieldByName(error.name);
|
|
1100
1695
|
if ($field) {
|
|
1101
1696
|
fieldsList.push($field);
|
|
1697
|
+
this.setValidationError($field, 'server', error['error-msg'], 'server');
|
|
1102
1698
|
this.showError($field, error['error-msg']);
|
|
1699
|
+
$field.removeAttribute('aria-busy');
|
|
1700
|
+
this.setFieldValidationState($field, 'invalid');
|
|
1103
1701
|
}
|
|
1104
1702
|
else {
|
|
1105
1703
|
console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
|
|
@@ -1107,6 +1705,8 @@ class Form {
|
|
|
1107
1705
|
});
|
|
1108
1706
|
if (this.config.scrollToFirstErroredInput === true)
|
|
1109
1707
|
this.scrollToFirstErroredInput(fieldsList);
|
|
1708
|
+
if (this.config.focusFirstErroredInput === true)
|
|
1709
|
+
this.focusFirstErroredInput(fieldsList);
|
|
1110
1710
|
}
|
|
1111
1711
|
}
|
|
1112
1712
|
sendData() {
|
|
@@ -1167,12 +1767,8 @@ class Form {
|
|
|
1167
1767
|
}
|
|
1168
1768
|
submit() {
|
|
1169
1769
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1170
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1770
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
1171
1771
|
if (!this.waitResponse) {
|
|
1172
|
-
if (this.config.showLoaderButton === true)
|
|
1173
|
-
this.showLoader();
|
|
1174
|
-
(_b = (_a = this.config).onSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
1175
|
-
this.waitResponse = true;
|
|
1176
1772
|
let loadingFinished = false;
|
|
1177
1773
|
const finishLoading = () => {
|
|
1178
1774
|
if (loadingFinished)
|
|
@@ -1183,27 +1779,44 @@ class Form {
|
|
|
1183
1779
|
loadingFinished = true;
|
|
1184
1780
|
};
|
|
1185
1781
|
try {
|
|
1782
|
+
const beforeSubmitResult = yield ((_b = (_a = this.config).onBeforeSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, this));
|
|
1783
|
+
if (beforeSubmitResult === false) {
|
|
1784
|
+
return {
|
|
1785
|
+
success: false,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (this.config.showLoaderButton === true)
|
|
1789
|
+
this.showLoader();
|
|
1790
|
+
(_d = (_c = this.config).onSubmit) === null || _d === void 0 ? void 0 : _d.call(_c, this);
|
|
1791
|
+
this.waitResponse = true;
|
|
1186
1792
|
const response = yield this.sendData();
|
|
1187
1793
|
const responseBody = yield this.parseResponseBody(response);
|
|
1188
1794
|
finishLoading();
|
|
1189
|
-
(
|
|
1795
|
+
(_f = (_e = this.config).onResponse) === null || _f === void 0 ? void 0 : _f.call(_e, responseBody, this);
|
|
1190
1796
|
if (response.ok && responseBody.success === true) {
|
|
1191
|
-
(
|
|
1797
|
+
(_h = (_g = this.config).onResponseSuccess) === null || _h === void 0 ? void 0 : _h.call(_g, responseBody, this);
|
|
1192
1798
|
}
|
|
1193
1799
|
else {
|
|
1194
|
-
(
|
|
1800
|
+
(_k = (_j = this.config).onResponseUnsuccess) === null || _k === void 0 ? void 0 : _k.call(_j, responseBody, this);
|
|
1195
1801
|
this.handleUnsuccessfulResponse(responseBody);
|
|
1196
1802
|
}
|
|
1197
1803
|
parseCommonResponseProperties(responseBody);
|
|
1804
|
+
return {
|
|
1805
|
+
success: response.ok && responseBody.success === true,
|
|
1806
|
+
response,
|
|
1807
|
+
responseBody,
|
|
1808
|
+
};
|
|
1198
1809
|
}
|
|
1199
1810
|
catch (error) {
|
|
1200
1811
|
finishLoading();
|
|
1812
|
+
(_m = (_l = this.config).onSubmitError) === null || _m === void 0 ? void 0 : _m.call(_l, error, this);
|
|
1201
1813
|
throw error;
|
|
1202
1814
|
}
|
|
1203
1815
|
}
|
|
1816
|
+
return undefined;
|
|
1204
1817
|
});
|
|
1205
1818
|
}
|
|
1206
1819
|
}
|
|
1207
1820
|
|
|
1208
|
-
export { blockScrollBody, closest, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
1821
|
+
export { FORM_ERROR_FIELD, blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
1209
1822
|
//# sourceMappingURL=index.js.map
|